From 8979a406963cfa9940feac59e7df56cc09fe0794 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist Date: Mon, 18 Dec 2023 16:54:25 -0700 Subject: [PATCH 01/19] fixes for entity display --- src/app/core/patents/patent.tsx | 3 ++- src/types/entities.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/core/patents/patent.tsx b/src/app/core/patents/patent.tsx index b209fe6..ae7ee09 100644 --- a/src/app/core/patents/patent.tsx +++ b/src/app/core/patents/patent.tsx @@ -143,8 +143,9 @@ const getPatentColumns = (): GridColDef[] => [ export const PatentList = async (args: PatentSearchArgs) => { const columns = getPatentColumns(); - const patents = await fetchPatents(args); try { + const patents = await fetchPatents(args); + return ( Date: Tue, 19 Dec 2023 13:06:40 -0700 Subject: [PATCH 02/19] refactoring location of composite components --- .../core/{patents => dashboard}/actions.ts | 2 +- src/app/core/dashboard/asset.tsx | 47 +++ .../core/{patents => dashboard}/content.tsx | 9 +- .../{patents => dashboard}/description.tsx | 0 src/app/core/{patents => dashboard}/graph.tsx | 0 .../core/{patents => dashboard}/over-time.tsx | 2 +- src/app/core/{patents => dashboard}/page.tsx | 0 src/app/core/dashboard/patent.tsx | 47 +++ .../core/{patents => dashboard}/search.tsx | 0 .../core/{patents => dashboard}/summary.tsx | 2 +- src/app/core/dashboard/trials.tsx | 71 ++++ src/app/core/{patents => dashboard}/types.ts | 0 src/app/core/patents/client.tsx | 359 ------------------ src/app/core/patents/compound.tsx | 98 ----- src/app/core/patents/trials.tsx | 180 --------- src/components/charts/sparklines/bar.tsx | 42 -- src/components/charts/sparklines/line.tsx | 32 -- src/components/composite/assets/client.tsx | 50 +++ .../composite/config.ts} | 189 +++++++-- src/components/composite/patents/client.tsx | 134 +++++++ src/components/composite/styles.tsx | 100 +++++ src/components/composite/trials/client.tsx | 146 +++++++ src/components/data/grid/formatters.tsx | 6 +- src/components/data/grid/grid.tsx | 54 ++- src/components/layout/title.tsx | 10 +- src/types/entities.ts | 3 +- 26 files changed, 798 insertions(+), 785 deletions(-) rename src/app/core/{patents => dashboard}/actions.ts (98%) create mode 100644 src/app/core/dashboard/asset.tsx rename src/app/core/{patents => dashboard}/content.tsx (93%) rename src/app/core/{patents => dashboard}/description.tsx (100%) rename src/app/core/{patents => dashboard}/graph.tsx (100%) rename src/app/core/{patents => dashboard}/over-time.tsx (97%) rename src/app/core/{patents => dashboard}/page.tsx (100%) create mode 100644 src/app/core/dashboard/patent.tsx rename src/app/core/{patents => dashboard}/search.tsx (100%) rename src/app/core/{patents => dashboard}/summary.tsx (97%) create mode 100644 src/app/core/dashboard/trials.tsx rename src/app/core/{patents => dashboard}/types.ts (100%) delete mode 100644 src/app/core/patents/client.tsx delete mode 100644 src/app/core/patents/compound.tsx delete mode 100644 src/app/core/patents/trials.tsx delete mode 100644 src/components/charts/sparklines/bar.tsx delete mode 100644 src/components/charts/sparklines/line.tsx create mode 100644 src/components/composite/assets/client.tsx rename src/{app/core/patents/patent.tsx => components/composite/config.ts} (50%) create mode 100644 src/components/composite/patents/client.tsx create mode 100644 src/components/composite/styles.tsx create mode 100644 src/components/composite/trials/client.tsx diff --git a/src/app/core/patents/actions.ts b/src/app/core/dashboard/actions.ts similarity index 98% rename from src/app/core/patents/actions.ts rename to src/app/core/dashboard/actions.ts index b2c1548..29490ae 100644 --- a/src/app/core/patents/actions.ts +++ b/src/app/core/dashboard/actions.ts @@ -80,7 +80,7 @@ export const fetchDescription = cache( * @param args * @returns patents promise */ -export const fetchEntities = cache( +export const fetchAssets = cache( async (args: EntitySearchArgs): Promise => { if (args.terms?.length === 0) { return []; diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx new file mode 100644 index 0000000..2f1ee3a --- /dev/null +++ b/src/app/core/dashboard/asset.tsx @@ -0,0 +1,47 @@ +'use server'; + +import Box from '@mui/joy/Box'; +import Alert from '@mui/joy/Alert'; +import Typography from '@mui/joy/Typography'; +import WarningIcon from '@mui/icons-material/Warning'; +import 'server-only'; + +import { DataGrid } from '@/components/data/grid'; +import { getAssetColumns } from '@/components/composite/config'; +import { AssetDetail } from '@/components/composite/assets/client'; +import { Entity, EntitySearchArgs } from '@/types/entities'; + +import { fetchAssets } from './actions'; + +export const AssetList = async (args: EntitySearchArgs) => { + const columns = getAssetColumns(); + try { + const assets = await fetchAssets(args); + return ( + + } + detailHeight={800} + rows={assets.map((asset) => ({ + ...asset, + id: asset.name, + }))} + /> + + ); + } catch (e) { + return ( + } + variant="soft" + color="warning" + > + Failed to fetch patents + + {e instanceof Error ? e.message : JSON.stringify(e)} + + + ); + } +}; diff --git a/src/app/core/patents/content.tsx b/src/app/core/dashboard/content.tsx similarity index 93% rename from src/app/core/patents/content.tsx rename to src/app/core/dashboard/content.tsx index 064b0b6..b7f74bd 100644 --- a/src/app/core/patents/content.tsx +++ b/src/app/core/dashboard/content.tsx @@ -10,22 +10,23 @@ import WarningIcon from '@mui/icons-material/Warning'; import { Tabs } from '@/components/layout/tabs'; import { PatentSearchArgs } from '@/types/patents'; -import { getStyles } from './client'; -import { CompoundList } from './compound'; +import { AssetList } from './asset'; import { PatentList } from './patent'; import { PatentGraph } from './graph'; import { OverTime } from './over-time'; import { Summary } from './summary'; import { TrialList } from './trials'; +import { getStyles } from '../../../components/composite/styles'; + export const Content = (args: PatentSearchArgs) => { try { const tabs = [ { - label: 'Compounds', + label: 'Assets', panel: ( }> - + ), }, diff --git a/src/app/core/patents/description.tsx b/src/app/core/dashboard/description.tsx similarity index 100% rename from src/app/core/patents/description.tsx rename to src/app/core/dashboard/description.tsx diff --git a/src/app/core/patents/graph.tsx b/src/app/core/dashboard/graph.tsx similarity index 100% rename from src/app/core/patents/graph.tsx rename to src/app/core/dashboard/graph.tsx diff --git a/src/app/core/patents/over-time.tsx b/src/app/core/dashboard/over-time.tsx similarity index 97% rename from src/app/core/patents/over-time.tsx rename to src/app/core/dashboard/over-time.tsx index c559771..dcce4f7 100644 --- a/src/app/core/patents/over-time.tsx +++ b/src/app/core/dashboard/over-time.tsx @@ -18,7 +18,7 @@ import { doFetch } from '@/utils/actions'; import { getQueryArgs } from '@/utils/patents'; import { formatLabel } from '@/utils/string'; -import { getStyles } from './client'; +import { getStyles } from '../../../components/composite/styles'; const fetchReports = cache( async (args: PatentSearchArgs): Promise => { diff --git a/src/app/core/patents/page.tsx b/src/app/core/dashboard/page.tsx similarity index 100% rename from src/app/core/patents/page.tsx rename to src/app/core/dashboard/page.tsx diff --git a/src/app/core/dashboard/patent.tsx b/src/app/core/dashboard/patent.tsx new file mode 100644 index 0000000..702b067 --- /dev/null +++ b/src/app/core/dashboard/patent.tsx @@ -0,0 +1,47 @@ +'use server'; + +import Box from '@mui/joy/Box'; +import Alert from '@mui/joy/Alert'; +import Typography from '@mui/joy/Typography'; +import WarningIcon from '@mui/icons-material/Warning'; +import 'server-only'; + +import { getPatentColumns } from '@/components/composite/config'; +import { PatentDetail } from '@/components/composite/patents/client'; +import { DataGrid } from '@/components/data/grid'; +import { Patent, PatentSearchArgs } from '@/types/patents'; + +import { fetchPatents } from './actions'; + +export const PatentList = async (args: PatentSearchArgs) => { + const columns = getPatentColumns(); + try { + const patents = await fetchPatents(args); + + return ( + + } + rows={patents.map((patent) => ({ + ...patent, + id: patent.publication_number, + }))} + /> + + ); + } catch (e) { + return ( + } + variant="soft" + color="warning" + > + Failed to fetch patents + + {e instanceof Error ? e.message : JSON.stringify(e)} + + + ); + } +}; diff --git a/src/app/core/patents/search.tsx b/src/app/core/dashboard/search.tsx similarity index 100% rename from src/app/core/patents/search.tsx rename to src/app/core/dashboard/search.tsx diff --git a/src/app/core/patents/summary.tsx b/src/app/core/dashboard/summary.tsx similarity index 97% rename from src/app/core/patents/summary.tsx rename to src/app/core/dashboard/summary.tsx index a2eb64e..8e6c09e 100644 --- a/src/app/core/patents/summary.tsx +++ b/src/app/core/dashboard/summary.tsx @@ -13,7 +13,7 @@ import { import { doFetch } from '@/utils/actions'; import { getQueryArgs } from '@/utils/patents'; -import { getStyles } from './client'; +import { getStyles } from '../../../components/composite/styles'; const fetchSummaries = cache( async (args: PatentSearchArgs): Promise => { diff --git a/src/app/core/dashboard/trials.tsx b/src/app/core/dashboard/trials.tsx new file mode 100644 index 0000000..a46a046 --- /dev/null +++ b/src/app/core/dashboard/trials.tsx @@ -0,0 +1,71 @@ +'use server'; + +import Box from '@mui/joy/Box'; +import Alert from '@mui/joy/Alert'; +import Typography from '@mui/joy/Typography'; +import WarningIcon from '@mui/icons-material/Warning'; +import 'server-only'; + +import { getTrialColumns } from '@/components/composite/config'; +import { TrialDetail } from '@/components/composite/trials/client'; +import { DataGrid } from '@/components/data/grid'; +import { Trial, TrialSearchArgs } from '@/types/trials'; + +import { fetchTrials } from './actions'; + +export const TrialList = async (args: TrialSearchArgs) => { + const columns = getTrialColumns(); + try { + const trials = await fetchTrials(args); + + return ( + + {/* + t.start_date && + t.end_date && + new Date(t.end_date).getTime() > + new Date().getTime() + ) + .map((t) => ({ + x: t.mesh_conditions[0], + y: [ + new Date(t.start_date || '').getTime(), + new Date(t.end_date || '').getTime(), + ], + })) + .slice(0, 200), + }, + ]} + /> */} + } + rows={trials.map((trial) => ({ + ...trial, + id: trial.nct_id, + }))} + /> + + ); + } catch (e) { + return ( + } + variant="soft" + color="warning" + > + Failed to fetch patents + + {e instanceof Error ? e.message : JSON.stringify(e)} + + + ); + } +}; diff --git a/src/app/core/patents/types.ts b/src/app/core/dashboard/types.ts similarity index 100% rename from src/app/core/patents/types.ts rename to src/app/core/dashboard/types.ts diff --git a/src/app/core/patents/client.tsx b/src/app/core/patents/client.tsx deleted file mode 100644 index edde6ed..0000000 --- a/src/app/core/patents/client.tsx +++ /dev/null @@ -1,359 +0,0 @@ -'use client'; - -/* eslint-disable @typescript-eslint/naming-convention */ - -import NextLink from 'next/link'; -import { usePathname } from 'next/navigation'; -import { Theme } from '@mui/joy/styles'; -import Divider from '@mui/joy/Divider'; -import Grid from '@mui/joy/Grid'; -import Link from '@mui/joy/Link'; -import List from '@mui/joy/List'; -import ListItem from '@mui/joy/ListItem'; -import ListItemDecorator from '@mui/joy/ListItemDecorator'; -import Typography from '@mui/joy/Typography'; -import { GridCellParams } from '@mui/x-data-grid/models/params/gridCellParams'; -import clsx from 'clsx'; -import unescape from 'lodash/fp/unescape'; - -import { Chips } from '@/components/data/chip'; -import { Metric } from '@/components/data/metric'; -import { Section } from '@/components/layout/section'; -import { Title } from '@/components/layout/title'; -import { Patent } from '@/types/patents'; -import { formatLabel, formatPercent, getSelectableId } from '@/utils/string'; -import { Trial } from '@/types/trials'; - -const SimilarPatents = ({ patent }: { patent: Patent }): JSX.Element => ( - <> - Similar Patents - - {patent.similar_patents - .filter((s) => s.startsWith('WO')) - .map((s, index) => ( - - · - - {s} - - - ( - - Search - - ) - - - ))} - - -); - -/** - * Detail content panel for patents grid - */ -export const PatentDetail = ({ - row: patent, -}: { - row: T; -}): JSX.Element => { - const domainsOfInterest: (keyof T)[] = [ - 'assignees', - 'attributes', - 'biologics', - 'compounds', - 'devices', - 'diseases', - 'inventors', - 'mechanisms', - ]; - const pathname = usePathname(); - const approvalInfo = patent.approval_dates - ? `\n\nApproved ${patent.approval_dates[0]}} for indication ${ - patent.indications?.[0] || '(unknown)' - } (${patent.brand_name}/${patent.generic_name}).` - : ''; - const trialInfo = patent.last_trial_status - ? `\n\nLast trial update: ${patent.last_trial_status} on ${ - patent.last_trial_update - }. NCTs ${(patent.nct_ids || []).join(', ')}.` - : ''; - return ( -
- - - {domainsOfInterest.map((domain) => ( - <Chips - baseUrl={pathname} - color="neutral" - label={formatLabel(domain as string)} - items={(patent[domain] as string[]) || []} - /> - ))} - - <Divider sx={{ my: 3 }} /> - <Grid container spacing={3}> - <Grid xs={6} sm={2}> - <Metric - value={patent.suitability_score} - label="Suitability" - tooltip={patent.suitability_score_explanation || ''} - /> - </Grid> - <Grid xs={6} sm={2}> - <Metric - value={patent.patent_years} - label="Patent Years Left" - /> - </Grid> - <Grid xs={6} sm={2}> - <Metric - value={patent.availability_likelihood} - label="Likehood of Availability" - tooltip={patent.availability_explanation} - /> - </Grid> - </Grid> - <Divider sx={{ my: 3 }} /> - <Section> - <SimilarPatents patent={patent} /> - </Section> - </Section> - ); -}; - -const OutcomesList = ({ trial }: { trial: Trial }): JSX.Element => ( - <> - <Typography level="title-md">Outcomes</Typography> - <List> - {trial.primary_outcomes.map((s, index) => ( - <ListItem key={`${getSelectableId(s)}-${index}`}> - <ListItemDecorator>·</ListItemDecorator> - {s} - </ListItem> - ))} - </List> - </> -); - -/** - * Detail content panel for patents grid - */ -export const TrialDetail = <T extends Trial>({ - row: trial, -}: { - row: T; -}): JSX.Element => { - const pathname = usePathname(); - const fields: (keyof T)[] = [ - 'conditions', - 'mesh_conditions', - 'interventions', - ]; - return ( - <Section mx={3}> - <Title - link={{ - label: trial.nct_id, - url: `https://clinicaltrials.gov/study/${trial.nct_id}`, - }} - title={trial.title} - description={`${ - trial.sponsor || 'Unknown sponsor' - } (${formatLabel(trial.sponsor_type)})`} - variant="soft" - /> - - {fields.map((field) => ( - <Chips - baseUrl={pathname} - color="primary" - label={formatLabel(field as string)} - items={(trial[field] as string[]) || []} - /> - ))} - - <Divider sx={{ my: 3 }} /> - <Grid container spacing={1}> - <Grid> - <Metric - color="primary" - label="Status" - value={trial.status} - /> - </Grid> - <Grid> - <Metric color="primary" label="Phase" value={trial.phase} /> - </Grid> - <Grid> - <Metric - color="primary" - label="Design" - value={trial.design} - /> - </Grid> - <Grid> - <Metric - color="primary" - label="Randomization" - value={trial.randomization} - /> - </Grid> - <Grid> - <Metric - color="primary" - label="Masking" - value={trial.masking} - /> - </Grid> - </Grid> - <Grid container mt={1} spacing={1}> - <Grid> - <Metric label="Enrollment" value={trial.enrollment || 0} /> - </Grid> - <Grid> - <Metric - formatter={(v) => `${v || '?'} days`} - label="Duration" - value={trial.duration || 0} - /> - </Grid> - <Grid> - <Metric - formatter={(v) => `${v || '?'} days`} - label="Outcome Timeframe" - value={trial.max_timeframe || 0} - /> - </Grid> - <Grid> - <Metric - value={ - trial.dropout_percent - ? formatPercent(trial.dropout_percent) - : '--' - } - label="Dropout Rate" - /> - </Grid> - <Grid> - <Metric - value={trial.termination_reason || '--'} - label="Termination Reason" - tooltip={trial.why_stopped || undefined} - /> - </Grid> - </Grid> - {trial.primary_outcomes.length > 0 && ( - <Section> - <OutcomesList trial={trial} /> - </Section> - )} - </Section> - ); -}; - -export const getPatentYearsClass = (params: GridCellParams<Patent>) => { - const { value } = params; - - if (typeof value !== 'number') { - return ''; - } - - return clsx('biosym-app', { - good: value > 15, - bad: value < 8, - }); -}; - -export const getAvailabilityClass = (params: GridCellParams<Patent>) => { - const { value } = params; - - if (typeof value !== 'string') { - return ''; - } - - return clsx('biosym-app', { - good: ['POSSIBLE', 'LIKELY'].includes(value), - bad: value === 'UNLIKELY', - }); -}; - -export const getScoresClassFunc = - ({ - goodThreshold = 0.75, - badThreshold = 0.2, - higherIsBetter = true, - }: { - goodThreshold?: number; - badThreshold?: number; - higherIsBetter?: boolean; - }) => - (params: GridCellParams<Patent>) => { - const { value } = params; - - if (typeof value !== 'number') { - return ''; - } - - return clsx('biosym-app', { - good: higherIsBetter - ? value > goodThreshold - : value < goodThreshold, - bad: higherIsBetter ? value < badThreshold : value > badThreshold, - }); - }; - -export const getScoresClass = getScoresClassFunc({}); -export const getTolerantScoresClass = getScoresClassFunc({ - goodThreshold: 0.42, - badThreshold: 0.2, -}); -export const getDropoutScoresClass = getScoresClassFunc({ - goodThreshold: 0.0, - badThreshold: 0.2, - higherIsBetter: false, -}); -export const getRepurposeScoreClass = getScoresClassFunc({ - goodThreshold: 0.2, - badThreshold: 0.0, -}); - -export const getStyles = ({ getColorSchemeSelector, palette }: Theme) => ({ - [getColorSchemeSelector('dark')]: { - '& .biosym-app.good': { - backgroundColor: palette.success[500], - fontWeight: '600', - filter: 'brightness(0.9)', - }, - '& .biosym-app.bad': { - backgroundColor: palette.danger[500], - fontWeight: '600', - filter: 'brightness(0.9)', - }, - }, - [getColorSchemeSelector('light')]: { - '& .biosym-app.good': { - backgroundColor: palette.success[100], - fontWeight: '600', - }, - '& .biosym-app.bad': { - backgroundColor: palette.danger[100], - fontWeight: '600', - }, - }, -}); diff --git a/src/app/core/patents/compound.tsx b/src/app/core/patents/compound.tsx deleted file mode 100644 index 28ecac4..0000000 --- a/src/app/core/patents/compound.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use server'; - -import Box from '@mui/joy/Box'; -import { GridColDef } from '@mui/x-data-grid/models/colDef'; -import Alert from '@mui/joy/Alert'; -import Typography from '@mui/joy/Typography'; -import WarningIcon from '@mui/icons-material/Warning'; -import 'server-only'; - -import { - DataGrid, - renderChip, - renderCompoundCountChip, - renderSparkline, -} from '@/components/data/grid'; -import { EntitySearchArgs } from '@/types/entities'; - -import { fetchEntities } from './actions'; - -const getCompoundColumns = (): GridColDef[] => [ - { - field: 'name', - headerName: 'Entity', - width: 250, - }, - { - field: 'trial_count', - headerName: 'Trials', - width: 125, - renderCell: renderCompoundCountChip, - }, - { - field: 'patent_count', - headerName: 'Patents', - width: 125, - renderCell: renderCompoundCountChip, - }, - { - field: 'activity', - headerName: 'Activity', - width: 125, - renderCell: renderSparkline, - }, - { - field: 'last_priority_year', - headerName: 'Latest Priority Date', - width: 125, - }, - { - field: 'max_phase', - headerName: 'Max Phase', - width: 125, - renderCell: renderChip, - }, - { - field: 'last_status', - headerName: 'Last Status', - width: 125, - renderCell: renderChip, - }, - { - field: 'last_updated', - headerName: 'Last Update', - width: 125, - }, -]; - -export const CompoundList = async (args: EntitySearchArgs) => { - const columns = getCompoundColumns(); - try { - const entities = await fetchEntities(args); - return ( - <Box height="100vh"> - <DataGrid - columns={columns} - // detailComponent={PatentDetail<Entity>} - rows={entities.map((entity) => ({ - ...entity, - id: entity.name, - }))} - /> - </Box> - ); - } catch (e) { - return ( - <Alert - startDecorator={<WarningIcon />} - variant="soft" - color="warning" - > - <Typography level="h4">Failed to fetch patents</Typography> - <Typography> - {e instanceof Error ? e.message : JSON.stringify(e)} - </Typography> - </Alert> - ); - } -}; diff --git a/src/app/core/patents/trials.tsx b/src/app/core/patents/trials.tsx deleted file mode 100644 index 5bd3c1f..0000000 --- a/src/app/core/patents/trials.tsx +++ /dev/null @@ -1,180 +0,0 @@ -'use server'; - -import Box from '@mui/joy/Box'; -import { GridColDef } from '@mui/x-data-grid/models/colDef'; -import Alert from '@mui/joy/Alert'; -import Typography from '@mui/joy/Typography'; -import WarningIcon from '@mui/icons-material/Warning'; -import 'server-only'; - -// import { Timeline } from '@/components/charts/gantt'; -import { - DataGrid, - formatName, - formatNumber, - formatYear, - renderChip, - renderPrimaryChip, - renderLabel, - renderPercent, -} from '@/components/data/grid'; -import { Trial, TrialSearchArgs } from '@/types/trials'; - -import { fetchTrials } from './actions'; -import { - TrialDetail, - getDropoutScoresClass, - getRepurposeScoreClass, -} from './client'; - -const getTrialColumns = (): GridColDef[] => [ - { field: 'nct_id', headerName: 'Nct Id', width: 135 }, - { - field: 'title', - headerName: 'Title', - width: 500, - }, - { - field: 'intervention', - headerName: 'Intervention', - renderCell: renderChip, - width: 200, - }, - { - field: 'condition', - headerName: 'Condition', - renderCell: renderChip, - width: 175, - }, - { - field: 'sponsor', - headerName: 'Sponsor', - width: 175, - valueFormatter: formatName, - }, - { - field: 'start_date', - headerName: 'Start', - width: 75, - valueFormatter: formatYear, - }, - { - field: 'end_date', - headerName: 'End', - width: 75, - valueFormatter: formatYear, - }, - { - field: 'phase', - headerName: 'Phase', - renderCell: renderChip, - width: 100, - }, - { - field: 'status', - headerName: 'Status', - renderCell: renderPrimaryChip, - width: 125, - }, - - { - field: 'dropout_percent', - headerName: 'Dropout %', - width: 100, - valueFormatter: renderPercent, - cellClassName: getDropoutScoresClass, - description: 'Dropout % = Dropouts / Enrollment', - }, - { - field: 'termination_reason', - headerName: 'Term. Reason', - width: 150, - }, - { - field: 'reformulation_score', - headerName: 'Repurpose⚠️', - width: 150, - valueFormatter: formatNumber, - cellClassName: getRepurposeScoreClass, - description: '**FAKE PLACEHOLDER**!! Esimated repurpose potential.', - }, - { - field: 'design', - headerName: 'Design', - width: 150, - valueFormatter: renderLabel, - }, - { - field: 'duration', - headerName: 'Duration', - width: 100, - valueFormatter: formatNumber, - }, - { - field: 'max_timeframe', - headerName: 'Timeframe', - width: 100, - }, - { - field: 'enrollment', - headerName: 'Enrollment', - width: 100, - valueFormatter: formatNumber, - }, -]; - -export const TrialList = async (args: TrialSearchArgs) => { - const columns = getTrialColumns(); - const trials = await fetchTrials(args); - try { - return ( - <Box height="100vh"> - {/* <Timeline - height={400} - pathname="" - series={[ - { - data: trials - .filter( - (t) => - t.start_date && - t.end_date && - new Date(t.end_date).getTime() > - new Date().getTime() - ) - .map((t) => ({ - x: t.mesh_conditions[0], - y: [ - new Date(t.start_date || '').getTime(), - new Date(t.end_date || '').getTime(), - ], - })) - .slice(0, 200), - }, - ]} - /> */} - <DataGrid - columns={columns} - detailComponent={TrialDetail<Trial>} - rows={trials.map((trial) => ({ - ...trial, - id: trial.nct_id, - }))} - /> - </Box> - ); - } catch (e) { - return ( - <Alert - startDecorator={<WarningIcon />} - variant="soft" - color="warning" - > - <Typography level="h4">Failed to fetch patents</Typography> - <Typography> - {e instanceof Error ? e.message : JSON.stringify(e)} - </Typography> - </Alert> - ); - } -}; diff --git a/src/components/charts/sparklines/bar.tsx b/src/components/charts/sparklines/bar.tsx deleted file mode 100644 index e39b4b2..0000000 --- a/src/components/charts/sparklines/bar.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Chart from 'react-apexcharts' - -import { ChartOptions, SparklineProps } from '../types' - -/** - * Sparkline bar - */ -export const Sparkbar = ({ - data, - height = 35, - width = 100, -}: SparklineProps): JSX.Element => { - const options: ChartOptions = { - tooltip: { - fixed: { enabled: false }, - x: { show: false }, - y: { title: { formatter: () => '' } }, - marker: { show: false }, - }, - plotOptions: { - bar: { - columnWidth: '80%', - }, - }, - xaxis: { - crosshairs: { - width: 1, - }, - }, - series: data, - } - - return ( - <Chart - height={height} - options={options} - sparkline={{ enabled: true }} - type="bar" - width={width} - /> - ) -} diff --git a/src/components/charts/sparklines/line.tsx b/src/components/charts/sparklines/line.tsx deleted file mode 100644 index 9fc6615..0000000 --- a/src/components/charts/sparklines/line.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import Chart from 'react-apexcharts' - -import { ChartOptions, SparklineProps } from '../types' - -/** - * Sparkline line - */ -export const Sparkline = ({ - data, - height = 35, - width = 100, -}: SparklineProps): JSX.Element => { - const options: ChartOptions = { - tooltip: { - fixed: { enabled: false }, - x: { show: false }, - y: { title: { formatter: () => '' } }, - marker: { show: false }, - }, - series: data, - } - - return ( - <Chart - height={height} - options={options} - sparkline={{ enabled: true }} - type="line" - width={width} - /> - ) -} diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx new file mode 100644 index 0000000..c4de254 --- /dev/null +++ b/src/components/composite/assets/client.tsx @@ -0,0 +1,50 @@ +'use server'; + +import { DataGrid } from '@/components/data/grid'; +import { Section } from '@/components/layout/section'; +import { Title } from '@/components/layout/title'; +import { Patent } from '@/types/patents'; +import { Trial } from '@/types/trials'; +import { Entity } from '@/types/entities'; + +import { getPatentColumns, getTrialColumns } from '../config'; + +/** + * Detail content panel for patents grid + */ +export const AssetDetail = <T extends Entity>({ + row: asset, +}: { + row: T; +}): JSX.Element => { + const patentColumns = getPatentColumns(); + const trialColumns = getTrialColumns(); + return ( + <Section mx={3}> + <Title description="" title={asset.name} variant="soft" /> + + {asset.patents.length > 0 && ( + <DataGrid + columns={patentColumns} + rows={asset.patents.map((patent: Patent) => ({ + ...patent, + id: patent.publication_number, + }))} + title="Patents" + variant="minimal" + /> + )} + {asset.trials.length > 0 && ( + <DataGrid + columns={trialColumns} + rows={asset.trials.map((trial: Trial) => ({ + ...trial, + id: trial.nct_id, + }))} + title="Trials" + variant="minimal" + /> + )} + </Section> + ); +}; diff --git a/src/app/core/patents/patent.tsx b/src/components/composite/config.ts similarity index 50% rename from src/app/core/patents/patent.tsx rename to src/components/composite/config.ts index ae7ee09..60f33d8 100644 --- a/src/app/core/patents/patent.tsx +++ b/src/components/composite/config.ts @@ -1,35 +1,34 @@ 'use server'; -import Box from '@mui/joy/Box'; import { GridColDef } from '@mui/x-data-grid/models/colDef'; -import Alert from '@mui/joy/Alert'; -import Typography from '@mui/joy/Typography'; -import WarningIcon from '@mui/icons-material/Warning'; import 'server-only'; import { - DataGrid, formatBlank, formatDate, formatName, formatNumber, formatYear, + renderAssetCountChip, renderBoolean, + renderChip, + renderLabel, + renderPrimaryChip, renderPercent, + renderSparkline, unencodeHtml, } from '@/components/data/grid'; -import { Patent, PatentSearchArgs } from '@/types/patents'; import { - PatentDetail, + getAvailabilityClass, + getDropoutScoresClass, getPatentYearsClass, + getRepurposeScoreClass, getScoresClass, getTolerantScoresClass, - getAvailabilityClass, -} from './client'; -import { fetchPatents } from './actions'; +} from './styles'; -const getPatentColumns = (): GridColDef[] => [ +export const getPatentColumns = (): GridColDef[] => [ { field: 'publication_number', headerName: 'Pub #', width: 170 }, { field: 'title', @@ -141,35 +140,141 @@ const getPatentColumns = (): GridColDef[] => [ }, ]; -export const PatentList = async (args: PatentSearchArgs) => { - const columns = getPatentColumns(); - try { - const patents = await fetchPatents(args); +export const getTrialColumns = (): GridColDef[] => [ + { field: 'nct_id', headerName: 'Nct Id', width: 135 }, + { + field: 'title', + headerName: 'Title', + width: 500, + }, + { + field: 'intervention', + headerName: 'Intervention', + renderCell: renderChip, + width: 200, + }, + { + field: 'condition', + headerName: 'Condition', + renderCell: renderChip, + width: 175, + }, + { + field: 'sponsor', + headerName: 'Sponsor', + width: 175, + valueFormatter: formatName, + }, + { + field: 'start_date', + headerName: 'Start', + width: 75, + valueFormatter: formatYear, + }, + { + field: 'end_date', + headerName: 'End', + width: 75, + valueFormatter: formatYear, + }, + { + field: 'phase', + headerName: 'Phase', + renderCell: renderChip, + width: 100, + }, + { + field: 'status', + headerName: 'Status', + renderCell: renderPrimaryChip, + width: 125, + }, - return ( - <Box height="100vh"> - <DataGrid - columns={columns} - detailComponent={PatentDetail<Patent>} - rows={patents.map((patent) => ({ - ...patent, - id: patent.publication_number, - }))} - /> - </Box> - ); - } catch (e) { - return ( - <Alert - startDecorator={<WarningIcon />} - variant="soft" - color="warning" - > - <Typography level="h4">Failed to fetch patents</Typography> - <Typography> - {e instanceof Error ? e.message : JSON.stringify(e)} - </Typography> - </Alert> - ); - } -}; + { + field: 'dropout_percent', + headerName: 'Dropout %', + width: 100, + valueFormatter: renderPercent, + cellClassName: getDropoutScoresClass, + description: 'Dropout % = Dropouts / Enrollment', + }, + { + field: 'termination_reason', + headerName: 'Term. Reason', + width: 150, + }, + { + field: 'reformulation_score', + headerName: 'Repurpose⚠️', + width: 150, + valueFormatter: formatNumber, + cellClassName: getRepurposeScoreClass, + description: '**FAKE PLACEHOLDER**!! Esimated repurpose potential.', + }, + { + field: 'design', + headerName: 'Design', + width: 150, + valueFormatter: renderLabel, + }, + { + field: 'duration', + headerName: 'Duration', + width: 100, + valueFormatter: formatNumber, + }, + { + field: 'max_timeframe', + headerName: 'Timeframe', + width: 100, + }, + { + field: 'enrollment', + headerName: 'Enrollment', + width: 100, + valueFormatter: formatNumber, + }, +]; + +export const getAssetColumns = (): GridColDef[] => [ + { + field: 'name', + headerName: 'Asset or Target', + width: 250, + }, + { + field: 'trial_count', + headerName: 'Trials', + width: 125, + renderCell: renderAssetCountChip, + }, + { + field: 'patent_count', + headerName: 'Patents', + width: 125, + renderCell: renderAssetCountChip, + }, + { + field: 'activity', + headerName: 'Activity', + width: 125, + renderCell: renderSparkline, + }, + { + field: 'last_updated', + headerName: 'Last Update', + width: 125, + }, + { + field: 'max_phase', + headerName: 'Max Phase', + width: 125, + renderCell: renderChip, + }, + { + field: 'last_status', + headerName: 'Last Status', + width: 125, + renderCell: renderChip, + }, +]; diff --git a/src/components/composite/patents/client.tsx b/src/components/composite/patents/client.tsx new file mode 100644 index 0000000..8e642aa --- /dev/null +++ b/src/components/composite/patents/client.tsx @@ -0,0 +1,134 @@ +'use client'; + +/* eslint-disable @typescript-eslint/naming-convention */ + +import NextLink from 'next/link'; +import { usePathname } from 'next/navigation'; +import Divider from '@mui/joy/Divider'; +import Grid from '@mui/joy/Grid'; +import Link from '@mui/joy/Link'; +import List from '@mui/joy/List'; +import ListItem from '@mui/joy/ListItem'; +import ListItemDecorator from '@mui/joy/ListItemDecorator'; +import Typography from '@mui/joy/Typography'; +import unescape from 'lodash/fp/unescape'; + +import { Chips } from '@/components/data/chip'; +import { Metric } from '@/components/data/metric'; +import { Section } from '@/components/layout/section'; +import { Title } from '@/components/layout/title'; +import { Patent } from '@/types/patents'; +import { formatLabel, getSelectableId } from '@/utils/string'; + +const SimilarPatents = ({ patent }: { patent: Patent }): JSX.Element => ( + <> + <Typography level="title-md">Similar Patents</Typography> + <List> + {patent.similar_patents + .filter((s) => s.startsWith('WO')) + .map((s, index) => ( + <ListItem key={`${getSelectableId(s)}-${index}`}> + <ListItemDecorator>·</ListItemDecorator> + <Link + component={NextLink} + href={patent.url} + target="_blank" + > + {s} + </Link> + <Typography level="body-sm" sx={{ ml: 1 }}> + ( + <Link + component={NextLink} + href={`/core/patents?terms=${s}`} + target="_blank" + > + Search + </Link> + ) + </Typography> + </ListItem> + ))} + </List> + </> +); + +/** + * Detail content panel for patents grid + */ +export const PatentDetail = <T extends Patent>({ + row: patent, +}: { + row: T; +}): JSX.Element => { + const domainsOfInterest: (keyof T)[] = [ + 'assignees', + 'attributes', + 'biologics', + 'compounds', + 'devices', + 'diseases', + 'inventors', + 'mechanisms', + ]; + const pathname = usePathname(); + const approvalInfo = patent.approval_dates + ? `\n\nApproved ${patent.approval_dates[0]}} for indication ${ + patent.indications?.[0] || '(unknown)' + } (${patent.brand_name}/${patent.generic_name}).` + : ''; + const trialInfo = patent.last_trial_status + ? `\n\nLast trial update: ${patent.last_trial_status} on ${ + patent.last_trial_update + }. NCTs ${(patent.nct_ids || []).join(', ')}.` + : ''; + return ( + <Section mx={3}> + <Title + description={`${unescape( + patent.abstract + )}${approvalInfo}${trialInfo}`} + link={{ label: patent.publication_number, url: patent.url }} + title={unescape(patent.title)} + variant="soft" + /> + + {domainsOfInterest.map((domain) => ( + <Chips + baseUrl={pathname} + color="neutral" + label={formatLabel(domain as string)} + items={(patent[domain] as string[]) || []} + /> + ))} + + <Divider sx={{ my: 3 }} /> + <Grid container spacing={3}> + <Grid xs={6} sm={2}> + <Metric + value={patent.suitability_score} + label="Suitability" + tooltip={patent.suitability_score_explanation || ''} + /> + </Grid> + <Grid xs={6} sm={2}> + <Metric + value={patent.patent_years} + label="Patent Years Left" + /> + </Grid> + <Grid xs={6} sm={2}> + <Metric + value={patent.availability_likelihood} + label="Likehood of Availability" + tooltip={patent.availability_explanation} + /> + </Grid> + </Grid> + <Divider sx={{ my: 3 }} /> + <Section> + <SimilarPatents patent={patent} /> + </Section> + </Section> + ); +}; diff --git a/src/components/composite/styles.tsx b/src/components/composite/styles.tsx new file mode 100644 index 0000000..f4111c8 --- /dev/null +++ b/src/components/composite/styles.tsx @@ -0,0 +1,100 @@ +'use client'; + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { Theme } from '@mui/joy/styles'; +import { GridCellParams } from '@mui/x-data-grid/models/params/gridCellParams'; +import clsx from 'clsx'; + +import { Patent } from '@/types/patents'; + +export const getPatentYearsClass = (params: GridCellParams<Patent>) => { + const { value } = params; + + if (typeof value !== 'number') { + return ''; + } + + return clsx('biosym-app', { + good: value > 15, + bad: value < 8, + }); +}; + +export const getAvailabilityClass = (params: GridCellParams<Patent>) => { + const { value } = params; + + if (typeof value !== 'string') { + return ''; + } + + return clsx('biosym-app', { + good: ['POSSIBLE', 'LIKELY'].includes(value), + bad: value === 'UNLIKELY', + }); +}; + +export const getScoresClassFunc = + ({ + goodThreshold = 0.75, + badThreshold = 0.2, + higherIsBetter = true, + }: { + goodThreshold?: number; + badThreshold?: number; + higherIsBetter?: boolean; + }) => + (params: GridCellParams<Patent>) => { + const { value } = params; + + if (typeof value !== 'number') { + return ''; + } + + return clsx('biosym-app', { + good: higherIsBetter + ? value > goodThreshold + : value < goodThreshold, + bad: higherIsBetter ? value < badThreshold : value > badThreshold, + }); + }; + +export const getScoresClass = getScoresClassFunc({}); +export const getTolerantScoresClass = getScoresClassFunc({ + goodThreshold: 0.42, + badThreshold: 0.2, +}); +export const getDropoutScoresClass = getScoresClassFunc({ + goodThreshold: 0.0, + badThreshold: 0.2, + higherIsBetter: false, +}); +export const getRepurposeScoreClass = getScoresClassFunc({ + goodThreshold: 0.2, + badThreshold: 0.0, +}); + +export const getStyles = ({ getColorSchemeSelector, palette }: Theme) => ({ + [getColorSchemeSelector('dark')]: { + '& .biosym-app.good': { + backgroundColor: palette.success[500], + fontWeight: '600', + filter: 'brightness(0.9)', + }, + '& .biosym-app.bad': { + backgroundColor: palette.danger[500], + fontWeight: '600', + filter: 'brightness(0.9)', + }, + }, + [getColorSchemeSelector('light')]: { + '& .biosym-app.good': { + backgroundColor: palette.success[100], + fontWeight: '600', + }, + '& .biosym-app.bad': { + backgroundColor: palette.danger[100], + fontWeight: '600', + }, + }, +}); diff --git a/src/components/composite/trials/client.tsx b/src/components/composite/trials/client.tsx new file mode 100644 index 0000000..9ba4347 --- /dev/null +++ b/src/components/composite/trials/client.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import Divider from '@mui/joy/Divider'; +import Grid from '@mui/joy/Grid'; +import ListItem from '@mui/joy/ListItem'; +import ListItemDecorator from '@mui/joy/ListItemDecorator'; +import List from '@mui/joy/List'; +import Typography from '@mui/joy/Typography'; + +import { Chips } from '@/components/data/chip'; +import { Metric } from '@/components/data/metric'; +import { Section } from '@/components/layout/section'; +import { Title } from '@/components/layout/title'; +import { formatLabel, formatPercent, getSelectableId } from '@/utils/string'; +import { Trial } from '@/types/trials'; + +const OutcomesList = ({ trial }: { trial: Trial }): JSX.Element => ( + <> + <Typography level="title-md">Outcomes</Typography> + <List> + {trial.primary_outcomes.map((s, index) => ( + <ListItem key={`${getSelectableId(s)}-${index}`}> + <ListItemDecorator>·</ListItemDecorator> + {s} + </ListItem> + ))} + </List> + </> +); + +/** + * Detail content panel for patents grid + */ +export const TrialDetail = <T extends Trial>({ + row: trial, +}: { + row: T; +}): JSX.Element => { + const pathname = usePathname(); + const fields: (keyof T)[] = [ + 'conditions', + 'mesh_conditions', + 'interventions', + ]; + return ( + <Section mx={3}> + <Title + link={{ + label: trial.nct_id, + url: `https://clinicaltrials.gov/study/${trial.nct_id}`, + }} + title={trial.title} + description={`${ + trial.sponsor || 'Unknown sponsor' + } (${formatLabel(trial.sponsor_type)})`} + variant="soft" + /> + + {fields.map((field) => ( + <Chips + baseUrl={pathname} + color="primary" + label={formatLabel(field as string)} + items={(trial[field] as string[]) || []} + /> + ))} + + <Divider sx={{ my: 3 }} /> + <Grid container spacing={1}> + <Grid> + <Metric + color="primary" + label="Status" + value={trial.status} + /> + </Grid> + <Grid> + <Metric color="primary" label="Phase" value={trial.phase} /> + </Grid> + <Grid> + <Metric + color="primary" + label="Design" + value={trial.design} + /> + </Grid> + <Grid> + <Metric + color="primary" + label="Randomization" + value={trial.randomization} + /> + </Grid> + <Grid> + <Metric + color="primary" + label="Masking" + value={trial.masking} + /> + </Grid> + </Grid> + <Grid container mt={1} spacing={1}> + <Grid> + <Metric label="Enrollment" value={trial.enrollment || 0} /> + </Grid> + <Grid> + <Metric + formatter={(v) => `${v || '?'} days`} + label="Duration" + value={trial.duration || 0} + /> + </Grid> + <Grid> + <Metric + formatter={(v) => `${v || '?'} days`} + label="Outcome Timeframe" + value={trial.max_timeframe || 0} + /> + </Grid> + <Grid> + <Metric + value={ + trial.dropout_percent + ? formatPercent(trial.dropout_percent) + : '--' + } + label="Dropout Rate" + /> + </Grid> + <Grid> + <Metric + value={trial.termination_reason || '--'} + label="Termination Reason" + tooltip={trial.why_stopped || undefined} + /> + </Grid> + </Grid> + {trial.primary_outcomes.length > 0 && ( + <Section> + <OutcomesList trial={trial} /> + </Section> + )} + </Section> + ); +}; diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 5e87ea1..7ab3585 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -165,20 +165,20 @@ export const getRenderChip = const href = getUrl(row); - if (!value) { + if (typeof value !== 'number' && !value) { return <span />; } return ( <Chip color={color} href={href}> - {formatLabel(value || '')} + {formatLabel(value)} </Chip> ); }; export const renderPrimaryChip = getRenderChip('primary'); export const renderChip = getRenderChip('neutral'); -export const renderCompoundCountChip = getRenderChip( +export const renderAssetCountChip = getRenderChip( 'primary', (row: { name: string }) => `/core/patents?terms=${row.name}` ); diff --git a/src/components/data/grid/grid.tsx b/src/components/data/grid/grid.tsx index 3ad3c0c..8677f19 100644 --- a/src/components/data/grid/grid.tsx +++ b/src/components/data/grid/grid.tsx @@ -14,9 +14,12 @@ import { GridColDef, GridToolbar } from '@mui/x-data-grid'; type DataGridProps<T> = { columns?: GridColDef[]; detailComponent?: ({ row }: { row: T }) => JSX.Element; + detailHeight?: number; initialState?: MuiDataGridProps['initialState']; isLoading?: MuiDataGridProps['loading']; rows: MuiDataGridProps['rows']; + title?: string; + variant?: 'standard' | 'minimal'; }; type Row = Record<string, unknown>; @@ -42,8 +45,11 @@ const NoRows = (): JSX.Element => ( export const DataGrid = <T extends Record<string, unknown>>({ columns: _columns, detailComponent, + detailHeight = 600, isLoading, rows, + variant, + title, ...props }: DataGridProps<T>) => { const DetailComponent = detailComponent || DetailPanelContent; @@ -53,25 +59,41 @@ export const DataGrid = <T extends Record<string, unknown>>({ ({ row }: { row: T }) => <DetailComponent row={row} />, [DetailComponent] ); - const getDetailPanelHeight = React.useCallback(() => 600, []); + const getDetailPanelHeight = React.useCallback( + () => detailHeight, + [detailHeight] + ); const columns = - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - _columns || Object.keys(rows[0]).map((field) => ({ field })); + _columns || + (rows.length > 0 + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(rows[0]).map((field) => ({ field })) + : []); return ( - <MuiDataGrid - {...props} - columns={columns} - getDetailPanelHeight={getDetailPanelHeight} - getDetailPanelContent={getDetailPanelContent} - loading={isLoading} - rows={rows} - slots={{ - toolbar: GridToolbar, - noRowsOverlay: NoRows, - }} - sx={{ border: 0 }} - /> + <> + {title && ( + <Typography level={variant === 'minimal' ? 'h4' : 'h3'}> + {title} + </Typography> + )} + <MuiDataGrid + {...props} + columns={columns} + density={variant === 'minimal' ? 'compact' : 'standard'} + getDetailPanelHeight={getDetailPanelHeight} + getDetailPanelContent={ + variant === 'minimal' ? undefined : getDetailPanelContent + } + loading={isLoading} + rows={rows} + slots={{ + toolbar: variant === 'minimal' ? null : GridToolbar, + noRowsOverlay: NoRows, + }} + sx={{ border: 0 }} + /> + </> ); }; diff --git a/src/components/layout/title.tsx b/src/components/layout/title.tsx index d0a060f..74f880f 100644 --- a/src/components/layout/title.tsx +++ b/src/components/layout/title.tsx @@ -14,7 +14,7 @@ export const Title = ({ }: { title: string; description?: string; - link: { label: string; url: string }; + link?: { label: string; url: string }; variant: TitleVariant; }) => ( <Sheet variant={variant} sx={{ mx: -3, mb: 2, mt: -3, px: 3, py: 2 }}> @@ -26,8 +26,10 @@ export const Title = ({ {description} </Typography> )} - <Link component={NextLink} href={link.url} target="_blank"> - {link.label} - </Link> + {link && ( + <Link component={NextLink} href={link.url} target="_blank"> + {link.label} + </Link> + )} </Sheet> ); diff --git a/src/types/entities.ts b/src/types/entities.ts index 01fceff..73e5fb8 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -7,10 +7,9 @@ import { TrialSchema } from './trials'; export const EntitySchema = z.object({ activity: z.array(z.number()), last_status: z.string(), - last_updated: z.union([z.string(), z.null()]), + last_updated: z.union([z.number(), z.null()]), name: z.string(), max_phase: z.string(), - last_priority_year: z.union([z.number(), z.string()]), patents: z.array(PatentSchema), patent_count: z.number(), record_count: z.number(), From 9b3dff25b6f40dcbba0805f8026a6188e20ef5cb Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 13:21:43 -0700 Subject: [PATCH 03/19] refactoring location of composite components --- src/app/core/dashboard/actions.ts | 136 ----------------------- src/app/core/dashboard/asset.tsx | 2 +- src/app/core/dashboard/content.tsx | 3 +- src/app/core/dashboard/description.tsx | 2 +- src/app/core/dashboard/page.tsx | 5 +- src/app/core/dashboard/patent.tsx | 2 +- src/app/core/dashboard/search.tsx | 145 ------------------------- src/app/core/dashboard/trials.tsx | 2 +- src/app/core/dashboard/types.ts | 7 -- src/types/patents.ts | 2 +- 10 files changed, 9 insertions(+), 297 deletions(-) delete mode 100644 src/app/core/dashboard/actions.ts delete mode 100644 src/app/core/dashboard/search.tsx delete mode 100644 src/app/core/dashboard/types.ts diff --git a/src/app/core/dashboard/actions.ts b/src/app/core/dashboard/actions.ts deleted file mode 100644 index 29490ae..0000000 --- a/src/app/core/dashboard/actions.ts +++ /dev/null @@ -1,136 +0,0 @@ -'use server'; - -import { cache } from 'react'; -import { z } from 'zod'; - -import { - ENTITY_SEARCH_API_URL, - PATENT_AUTOCOMPLETE_API_URL, - PATENT_SEARCH_API_URL, - TERM_DESCRIPTION_API_URL, - TRIAL_SEARCH_API_URL, -} from '@/constants'; -import { Option } from '@/types/select'; -import { - PatentResponse, - PatentResponseSchema, - PatentSearchArgs, -} from '@/types/patents'; -import { doFetch } from '@/utils/actions'; -import { getQueryArgs } from '@/utils/patents'; -import { - TrialResponse, - TrialResponseSchema, - TrialSearchArgs, -} from '@/types/trials'; -import { - EntityResponse, - EntityResponseSchema, - EntitySearchArgs, -} from '@/types/entities'; - -const AutocompleteResponse = z.array( - z.object({ - id: z.string(), - label: z.string(), - }) -); - -/** - * Autocomplete terms or ids from the API. - * @param str search string - * @returns options promise - */ -export const fetchAutocompletions = async ( - str: string, - mode: 'id' | 'term' = 'term' -): Promise<Option[]> => { - 'use server'; - - const res = await doFetch( - `${PATENT_AUTOCOMPLETE_API_URL}?mode=${mode}&string=${str}`, - AutocompleteResponse - ); - return res; -}; - -/** - * Fetch term(s) description from the API. Cached. - * @param terms - */ -export const fetchDescription = cache( - async (terms: string[]): Promise<string> => { - if (terms.length === 0) { - return ''; - } - const res = await fetch( - `${TERM_DESCRIPTION_API_URL}?terms=${terms.join(',')}` - ); - if (!res.ok) { - throw new Error( - `Failed to fetch description: ${res.status} ${res.statusText}` - ); - } - return res.text(); - } -); - -/** - * Fetch entities from the API. Cached. - * @param args - * @returns patents promise - */ -export const fetchAssets = cache( - async (args: EntitySearchArgs): Promise<EntityResponse> => { - if (args.terms?.length === 0) { - return []; - } - const queryArgs = getQueryArgs(args, true); - const res = await doFetch( - `${ENTITY_SEARCH_API_URL}?${queryArgs}`, - EntityResponseSchema - ); - - return res; - } -); - -/** - * Fetch patents from the API. Cached. - * @param args - * @returns patents promise - */ -export const fetchPatents = cache( - async (args: PatentSearchArgs): Promise<PatentResponse> => { - if (args.terms?.length === 0) { - return []; - } - const queryArgs = getQueryArgs(args, true); - const res = await doFetch( - `${PATENT_SEARCH_API_URL}?${queryArgs}`, - PatentResponseSchema - ); - - return res; - } -); - -/** - * Fetch patents from the API. Cached. - * @param args - * @returns patents promise - */ -export const fetchTrials = cache( - async (args: TrialSearchArgs): Promise<TrialResponse> => { - if (args.terms?.length === 0) { - return []; - } - const queryArgs = getQueryArgs(args, true); - const res = await doFetch( - `${TRIAL_SEARCH_API_URL}?${queryArgs}`, - TrialResponseSchema - ); - - return res; - } -); diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index 2f1ee3a..f4841b3 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -11,7 +11,7 @@ import { getAssetColumns } from '@/components/composite/config'; import { AssetDetail } from '@/components/composite/assets/client'; import { Entity, EntitySearchArgs } from '@/types/entities'; -import { fetchAssets } from './actions'; +import { fetchAssets } from '../actions'; export const AssetList = async (args: EntitySearchArgs) => { const columns = getAssetColumns(); diff --git a/src/app/core/dashboard/content.tsx b/src/app/core/dashboard/content.tsx index b7f74bd..9f2f62e 100644 --- a/src/app/core/dashboard/content.tsx +++ b/src/app/core/dashboard/content.tsx @@ -7,6 +7,7 @@ import Skeleton from '@mui/joy/Skeleton'; import Typography from '@mui/joy/Typography'; import WarningIcon from '@mui/icons-material/Warning'; +import { getStyles } from '@/components/composite/styles'; import { Tabs } from '@/components/layout/tabs'; import { PatentSearchArgs } from '@/types/patents'; @@ -17,8 +18,6 @@ import { OverTime } from './over-time'; import { Summary } from './summary'; import { TrialList } from './trials'; -import { getStyles } from '../../../components/composite/styles'; - export const Content = (args: PatentSearchArgs) => { try { const tabs = [ diff --git a/src/app/core/dashboard/description.tsx b/src/app/core/dashboard/description.tsx index ea015f5..ff94cc4 100644 --- a/src/app/core/dashboard/description.tsx +++ b/src/app/core/dashboard/description.tsx @@ -4,7 +4,7 @@ import Box from '@mui/joy/Box'; import ReactMarkdown from 'react-markdown'; import 'server-only'; -import { fetchDescription } from './actions'; +import { fetchDescription } from '../actions'; export const Description = async ({ terms }: { terms: string[] }) => { try { diff --git a/src/app/core/dashboard/page.tsx b/src/app/core/dashboard/page.tsx index b9fc52a..6d82bf2 100644 --- a/src/app/core/dashboard/page.tsx +++ b/src/app/core/dashboard/page.tsx @@ -4,13 +4,14 @@ import { Suspense } from 'react'; import Skeleton from '@mui/joy/Skeleton'; import Typography from '@mui/joy/Typography'; +import { SearchBar } from '@/components/composite'; import { Section } from '@/components/layout/section'; import { formatLabel } from '@/utils/string'; -import { fetchAutocompletions } from './actions'; import { Description } from './description'; import { Content } from './content'; -import { SearchBar } from './search'; + +import { fetchAutocompletions } from '../actions'; const Page = ({ searchParams }: { searchParams: Record<string, string> }) => { const terms = searchParams.terms?.split(';') ?? null; diff --git a/src/app/core/dashboard/patent.tsx b/src/app/core/dashboard/patent.tsx index 702b067..dd149c5 100644 --- a/src/app/core/dashboard/patent.tsx +++ b/src/app/core/dashboard/patent.tsx @@ -11,7 +11,7 @@ import { PatentDetail } from '@/components/composite/patents/client'; import { DataGrid } from '@/components/data/grid'; import { Patent, PatentSearchArgs } from '@/types/patents'; -import { fetchPatents } from './actions'; +import { fetchPatents } from '../actions'; export const PatentList = async (args: PatentSearchArgs) => { const columns = getPatentColumns(); diff --git a/src/app/core/dashboard/search.tsx b/src/app/core/dashboard/search.tsx deleted file mode 100644 index fee01a3..0000000 --- a/src/app/core/dashboard/search.tsx +++ /dev/null @@ -1,145 +0,0 @@ -'use client'; - -import { SetStateAction, useState } from 'react'; -import { usePathname } from 'next/navigation'; -import Grid from '@mui/joy/Grid'; - -import { Button } from '@/components/input/button'; -import { Autocomplete } from '@/components/input'; -import { Slider } from '@/components/input/slider'; -import { Section } from '@/components/layout/section'; -import { useNavigation } from '@/hooks/navigation'; -import { PatentSearchArgs } from '@/types/patents'; -import { Option } from '@/types/select'; -import { getQueryArgs } from '@/utils/patents'; -import { Select } from '@/components/input/select'; - -import { FetchAutocompletions } from './types'; - -export const SearchBar = ({ - exemplarPatents, - fetchAutocompletions, - minPatentYears, - queryType, - terms, -}: { - fetchAutocompletions: FetchAutocompletions; -} & PatentSearchArgs): JSX.Element => { - const { navigate } = useNavigation(); - const pathname = usePathname(); - const [newTerms, setTerms] = useState<string[] | null>(terms); - const [newExemplarPatents, setExemplarPatents] = useState<string[] | null>( - exemplarPatents - ); - const [newQueryType, setQueryType] = useState<string | null>(queryType); - const [newMinPatentYears, setMinPatentYears] = - useState<number>(minPatentYears); - - const handlePatentLifeChange = (value: number) => { - if (typeof value !== 'number') { - console.warn(`Invalid value: ${JSON.stringify(value)}`); - return; - } - setMinPatentYears(value); - }; - - return ( - <> - <Autocomplete<Option, true, false> - isMultiple - defaultValue={(terms || []).map((term) => ({ - id: term, - label: term, - }))} - isOptionEqualToValue={(option: Option, value: Option) => - option.id === value.id - } - label="Select Terms" - onChange={(e, values) => { - setTerms(values.map((v) => v.id)); - }} - optionFetcher={fetchAutocompletions} - size="xlg" - tooltip="Compounds, diseases, MoAs, pharmaceutical companies, etc." - variant="soft" - /> - <Section variant="l1"> - <Grid container spacing={4}> - <Grid xs={12} sm={4}> - <Slider - defaultValue={newMinPatentYears} - label="Minimum Patent Years Left" - min={0} - max={20} - onChange={(e, v) => - handlePatentLifeChange(v as number) - } - size="lg" - /> - </Grid> - <Grid xs={12} sm={2}> - <Select - defaultValue={queryType} - label="Search Type" - onChange={( - e: unknown, - value: SetStateAction<string | null> - ) => { - setQueryType(value); - }} - options={['AND', 'OR']} - /> - </Grid> - </Grid> - <Grid container spacing={4} sx={{ mt: 1 }}> - <Grid xs={12} sm={6}> - <Autocomplete<Option, true, false> - isMultiple - defaultValue={(exemplarPatents || []).map( - (patent) => ({ - id: patent, - label: patent, - }) - )} - isOptionEqualToValue={( - option: Option, - value: Option - ) => option.id === value.id} - label="Exemplar Patents" - onChange={(e, values) => { - setExemplarPatents(values.map((v) => v.id)); - }} - optionFetcher={(str: string) => - fetchAutocompletions(str, 'id') - } - size="md" - tooltip="Patents that exemplify what you are looking for, against which we'll perform cosine similarity comparisons against the embedded representations of the patents in our database." - variant="soft" - /> - </Grid> - </Grid> - </Section> - <Section variant="l2"> - <Button - onClick={() => { - if (!newTerms) { - console.debug("No terms selected, can't search"); - return; - } - const queryArgs = getQueryArgs({ - minPatentYears: newMinPatentYears, - queryType: newQueryType, - exemplarPatents: newExemplarPatents, - terms: newTerms, - }); - navigate(`${pathname}?${queryArgs}`); - }} - size="lg" - sx={{ ml: 'auto' }} - > - Search - </Button> - </Section> - </> - ); -}; diff --git a/src/app/core/dashboard/trials.tsx b/src/app/core/dashboard/trials.tsx index a46a046..7022b36 100644 --- a/src/app/core/dashboard/trials.tsx +++ b/src/app/core/dashboard/trials.tsx @@ -11,7 +11,7 @@ import { TrialDetail } from '@/components/composite/trials/client'; import { DataGrid } from '@/components/data/grid'; import { Trial, TrialSearchArgs } from '@/types/trials'; -import { fetchTrials } from './actions'; +import { fetchTrials } from '../actions'; export const TrialList = async (args: TrialSearchArgs) => { const columns = getTrialColumns(); diff --git a/src/app/core/dashboard/types.ts b/src/app/core/dashboard/types.ts deleted file mode 100644 index 399e9d4..0000000 --- a/src/app/core/dashboard/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Option } from '@/types/select'; - -export type AutocompleteMode = 'id' | 'term'; -export type FetchAutocompletions = ( - str: string, - mode?: AutocompleteMode -) => Promise<Option[]>; diff --git a/src/types/patents.ts b/src/types/patents.ts index d3fd92a..3e4fd03 100644 --- a/src/types/patents.ts +++ b/src/types/patents.ts @@ -70,7 +70,7 @@ export type PatentResponse = z.infer<typeof PatentResponseSchema>; export type PatentSearchArgs = { exemplarPatents: string[] | null; - minPatentYears: number; + minPatentYears: number | null; queryType: string | null; terms: string[] | null; }; From 520ec01628273b389ef935d3fb0250c2291dd774 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 13:21:57 -0700 Subject: [PATCH 04/19] refactoring location of composite components --- src/app/core/actions.ts | 136 ++++++++++++++++ src/app/core/asset/page.tsx | 32 ++++ src/components/composite/index.ts | 1 + .../composite/search/search-bar.tsx | 146 ++++++++++++++++++ src/components/composite/search/types.ts | 7 + src/types/actions.ts | 0 6 files changed, 322 insertions(+) create mode 100644 src/app/core/actions.ts create mode 100644 src/app/core/asset/page.tsx create mode 100644 src/components/composite/index.ts create mode 100644 src/components/composite/search/search-bar.tsx create mode 100644 src/components/composite/search/types.ts create mode 100644 src/types/actions.ts diff --git a/src/app/core/actions.ts b/src/app/core/actions.ts new file mode 100644 index 0000000..29490ae --- /dev/null +++ b/src/app/core/actions.ts @@ -0,0 +1,136 @@ +'use server'; + +import { cache } from 'react'; +import { z } from 'zod'; + +import { + ENTITY_SEARCH_API_URL, + PATENT_AUTOCOMPLETE_API_URL, + PATENT_SEARCH_API_URL, + TERM_DESCRIPTION_API_URL, + TRIAL_SEARCH_API_URL, +} from '@/constants'; +import { Option } from '@/types/select'; +import { + PatentResponse, + PatentResponseSchema, + PatentSearchArgs, +} from '@/types/patents'; +import { doFetch } from '@/utils/actions'; +import { getQueryArgs } from '@/utils/patents'; +import { + TrialResponse, + TrialResponseSchema, + TrialSearchArgs, +} from '@/types/trials'; +import { + EntityResponse, + EntityResponseSchema, + EntitySearchArgs, +} from '@/types/entities'; + +const AutocompleteResponse = z.array( + z.object({ + id: z.string(), + label: z.string(), + }) +); + +/** + * Autocomplete terms or ids from the API. + * @param str search string + * @returns options promise + */ +export const fetchAutocompletions = async ( + str: string, + mode: 'id' | 'term' = 'term' +): Promise<Option[]> => { + 'use server'; + + const res = await doFetch( + `${PATENT_AUTOCOMPLETE_API_URL}?mode=${mode}&string=${str}`, + AutocompleteResponse + ); + return res; +}; + +/** + * Fetch term(s) description from the API. Cached. + * @param terms + */ +export const fetchDescription = cache( + async (terms: string[]): Promise<string> => { + if (terms.length === 0) { + return ''; + } + const res = await fetch( + `${TERM_DESCRIPTION_API_URL}?terms=${terms.join(',')}` + ); + if (!res.ok) { + throw new Error( + `Failed to fetch description: ${res.status} ${res.statusText}` + ); + } + return res.text(); + } +); + +/** + * Fetch entities from the API. Cached. + * @param args + * @returns patents promise + */ +export const fetchAssets = cache( + async (args: EntitySearchArgs): Promise<EntityResponse> => { + if (args.terms?.length === 0) { + return []; + } + const queryArgs = getQueryArgs(args, true); + const res = await doFetch( + `${ENTITY_SEARCH_API_URL}?${queryArgs}`, + EntityResponseSchema + ); + + return res; + } +); + +/** + * Fetch patents from the API. Cached. + * @param args + * @returns patents promise + */ +export const fetchPatents = cache( + async (args: PatentSearchArgs): Promise<PatentResponse> => { + if (args.terms?.length === 0) { + return []; + } + const queryArgs = getQueryArgs(args, true); + const res = await doFetch( + `${PATENT_SEARCH_API_URL}?${queryArgs}`, + PatentResponseSchema + ); + + return res; + } +); + +/** + * Fetch patents from the API. Cached. + * @param args + * @returns patents promise + */ +export const fetchTrials = cache( + async (args: TrialSearchArgs): Promise<TrialResponse> => { + if (args.terms?.length === 0) { + return []; + } + const queryArgs = getQueryArgs(args, true); + const res = await doFetch( + `${TRIAL_SEARCH_API_URL}?${queryArgs}`, + TrialResponseSchema + ); + + return res; + } +); diff --git a/src/app/core/asset/page.tsx b/src/app/core/asset/page.tsx new file mode 100644 index 0000000..aeed587 --- /dev/null +++ b/src/app/core/asset/page.tsx @@ -0,0 +1,32 @@ +'use server'; + +import Typography from '@mui/joy/Typography'; + +import { fetchAutocompletions } from '@/app/core/actions'; +import { SearchBar } from '@/components/composite'; +import { Section } from '@/components/layout/section'; + +const Page = ({ searchParams }: { searchParams: Record<string, string> }) => { + const { asset } = searchParams; + + return ( + <> + <Section variant="separated"> + <SearchBar + exemplarPatents={null} + fetchAutocompletions={fetchAutocompletions} + minPatentYears={null} + queryType={null} + terms={null} + /> + </Section> + <Section variant="main"> + <Section> + <Typography level="h1">{asset}</Typography> + </Section> + </Section> + </> + ); +}; + +export default Page; diff --git a/src/components/composite/index.ts b/src/components/composite/index.ts new file mode 100644 index 0000000..dd98957 --- /dev/null +++ b/src/components/composite/index.ts @@ -0,0 +1 @@ +export { SearchBar } from './search/search-bar'; diff --git a/src/components/composite/search/search-bar.tsx b/src/components/composite/search/search-bar.tsx new file mode 100644 index 0000000..e2b204f --- /dev/null +++ b/src/components/composite/search/search-bar.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { SetStateAction, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import Grid from '@mui/joy/Grid'; + +import { Button } from '@/components/input/button'; +import { Autocomplete } from '@/components/input'; +import { Slider } from '@/components/input/slider'; +import { Section } from '@/components/layout/section'; +import { useNavigation } from '@/hooks/navigation'; +import { PatentSearchArgs } from '@/types/patents'; +import { Option } from '@/types/select'; +import { getQueryArgs } from '@/utils/patents'; +import { Select } from '@/components/input/select'; + +import { FetchAutocompletions } from './types'; + +export const SearchBar = ({ + exemplarPatents, + fetchAutocompletions, + minPatentYears, + queryType, + terms, +}: { + fetchAutocompletions: FetchAutocompletions; +} & PatentSearchArgs): JSX.Element => { + const { navigate } = useNavigation(); + const pathname = usePathname(); + const [newTerms, setTerms] = useState<string[] | null>(terms); + const [newExemplarPatents, setExemplarPatents] = useState<string[] | null>( + exemplarPatents + ); + const [newQueryType, setQueryType] = useState<string | null>(queryType); + const [newMinPatentYears, setMinPatentYears] = useState<number>( + minPatentYears || 0 + ); + + const handlePatentLifeChange = (value: number) => { + if (typeof value !== 'number') { + console.warn(`Invalid value: ${JSON.stringify(value)}`); + return; + } + setMinPatentYears(value); + }; + + return ( + <> + <Autocomplete<Option, true, false> + isMultiple + defaultValue={(terms || []).map((term) => ({ + id: term, + label: term, + }))} + isOptionEqualToValue={(option: Option, value: Option) => + option.id === value.id + } + label="Select Terms" + onChange={(e, values) => { + setTerms(values.map((v) => v.id)); + }} + optionFetcher={fetchAutocompletions} + size="xlg" + tooltip="Compounds, diseases, MoAs, pharmaceutical companies, etc." + variant="soft" + /> + <Section variant="l1"> + <Grid container spacing={4}> + <Grid xs={12} sm={4}> + <Slider + defaultValue={newMinPatentYears} + label="Minimum Patent Years Left" + min={0} + max={20} + onChange={(e, v) => + handlePatentLifeChange(v as number) + } + size="lg" + /> + </Grid> + <Grid xs={12} sm={2}> + <Select + defaultValue={queryType} + label="Search Type" + onChange={( + e: unknown, + value: SetStateAction<string | null> + ) => { + setQueryType(value); + }} + options={['AND', 'OR']} + /> + </Grid> + </Grid> + <Grid container spacing={4} sx={{ mt: 1 }}> + <Grid xs={12} sm={6}> + <Autocomplete<Option, true, false> + isMultiple + defaultValue={(exemplarPatents || []).map( + (patent) => ({ + id: patent, + label: patent, + }) + )} + isOptionEqualToValue={( + option: Option, + value: Option + ) => option.id === value.id} + label="Exemplar Patents" + onChange={(e, values) => { + setExemplarPatents(values.map((v) => v.id)); + }} + optionFetcher={(str: string) => + fetchAutocompletions(str, 'id') + } + size="md" + tooltip="Patents that exemplify what you are looking for, against which we'll perform cosine similarity comparisons against the embedded representations of the patents in our database." + variant="soft" + /> + </Grid> + </Grid> + </Section> + <Section variant="l2"> + <Button + onClick={() => { + if (!newTerms) { + console.debug("No terms selected, can't search"); + return; + } + const queryArgs = getQueryArgs({ + minPatentYears: newMinPatentYears, + queryType: newQueryType, + exemplarPatents: newExemplarPatents, + terms: newTerms, + }); + navigate(`${pathname}?${queryArgs}`); + }} + size="lg" + sx={{ ml: 'auto' }} + > + Search + </Button> + </Section> + </> + ); +}; diff --git a/src/components/composite/search/types.ts b/src/components/composite/search/types.ts new file mode 100644 index 0000000..399e9d4 --- /dev/null +++ b/src/components/composite/search/types.ts @@ -0,0 +1,7 @@ +import { Option } from '@/types/select'; + +export type AutocompleteMode = 'id' | 'term'; +export type FetchAutocompletions = ( + str: string, + mode?: AutocompleteMode +) => Promise<Option[]>; diff --git a/src/types/actions.ts b/src/types/actions.ts new file mode 100644 index 0000000..e69de29 From 1911d16be6a84cbf62d5c6bdf0cea3a9f3e2d59a Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 13:43:29 -0700 Subject: [PATCH 05/19] hiding panel content if not provided; variant --- src/components/data/grid/grid.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/data/grid/grid.tsx b/src/components/data/grid/grid.tsx index 8677f19..04dcc4b 100644 --- a/src/components/data/grid/grid.tsx +++ b/src/components/data/grid/grid.tsx @@ -11,6 +11,7 @@ import { } from '@mui/x-data-grid-pro'; import { GridColDef, GridToolbar } from '@mui/x-data-grid'; +type DataGridVariant = 'standard' | 'minimal' | 'maximal'; type DataGridProps<T> = { columns?: GridColDef[]; detailComponent?: ({ row }: { row: T }) => JSX.Element; @@ -19,7 +20,7 @@ type DataGridProps<T> = { isLoading?: MuiDataGridProps['loading']; rows: MuiDataGridProps['rows']; title?: string; - variant?: 'standard' | 'minimal'; + variant?: DataGridVariant; }; type Row = Record<string, unknown>; @@ -42,6 +43,16 @@ const NoRows = (): JSX.Element => ( <Typography level="h3">no results</Typography> ); +const getDensity = (variant: DataGridVariant): MuiDataGridProps['density'] => { + if (variant === 'minimal') { + return 'compact'; + } + if (variant === 'maximal') { + return 'comfortable'; + } + return 'standard'; +}; + export const DataGrid = <T extends Record<string, unknown>>({ columns: _columns, detailComponent, @@ -66,10 +77,11 @@ export const DataGrid = <T extends Record<string, unknown>>({ const columns = _columns || - (rows.length > 0 - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - Object.keys(rows[0]).map((field) => ({ field })) - : []); + Object.keys((rows?.[0] as Record<string, unknown>) || {}).map( + (field) => ({ field }) + ); + + const density = getDensity(variant || 'standard'); return ( <> @@ -81,10 +93,10 @@ export const DataGrid = <T extends Record<string, unknown>>({ <MuiDataGrid {...props} columns={columns} - density={variant === 'minimal' ? 'compact' : 'standard'} + density={density} getDetailPanelHeight={getDetailPanelHeight} getDetailPanelContent={ - variant === 'minimal' ? undefined : getDetailPanelContent + detailComponent ? getDetailPanelContent : undefined } loading={isLoading} rows={rows} From 4932531fc6323b36309fc23f44c9a5b599dbc21d Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 13:44:15 -0700 Subject: [PATCH 06/19] render main grid content; disabling of heavy asset view --- src/app/core/dashboard/asset.tsx | 6 ++-- src/components/composite/assets/client.tsx | 42 +++------------------- src/components/composite/config.ts | 2 ++ src/components/data/grid/formatters.tsx | 13 +++++++ 4 files changed, 23 insertions(+), 40 deletions(-) diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index f4841b3..ecb570f 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -8,8 +8,7 @@ import 'server-only'; import { DataGrid } from '@/components/data/grid'; import { getAssetColumns } from '@/components/composite/config'; -import { AssetDetail } from '@/components/composite/assets/client'; -import { Entity, EntitySearchArgs } from '@/types/entities'; +import { EntitySearchArgs } from '@/types/entities'; import { fetchAssets } from '../actions'; @@ -21,12 +20,13 @@ export const AssetList = async (args: EntitySearchArgs) => { <Box height="100vh"> <DataGrid columns={columns} - detailComponent={AssetDetail<Entity>} + // detailComponent={AssetDetail<Entity>} detailHeight={800} rows={assets.map((asset) => ({ ...asset, id: asset.name, }))} + variant="maximal" /> </Box> ); diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx index c4de254..00370a2 100644 --- a/src/components/composite/assets/client.tsx +++ b/src/components/composite/assets/client.tsx @@ -1,14 +1,9 @@ 'use server'; -import { DataGrid } from '@/components/data/grid'; import { Section } from '@/components/layout/section'; import { Title } from '@/components/layout/title'; -import { Patent } from '@/types/patents'; -import { Trial } from '@/types/trials'; import { Entity } from '@/types/entities'; -import { getPatentColumns, getTrialColumns } from '../config'; - /** * Detail content panel for patents grid */ @@ -16,35 +11,8 @@ export const AssetDetail = <T extends Entity>({ row: asset, }: { row: T; -}): JSX.Element => { - const patentColumns = getPatentColumns(); - const trialColumns = getTrialColumns(); - return ( - <Section mx={3}> - <Title description="" title={asset.name} variant="soft" /> - - {asset.patents.length > 0 && ( - <DataGrid - columns={patentColumns} - rows={asset.patents.map((patent: Patent) => ({ - ...patent, - id: patent.publication_number, - }))} - title="Patents" - variant="minimal" - /> - )} - {asset.trials.length > 0 && ( - <DataGrid - columns={trialColumns} - rows={asset.trials.map((trial: Trial) => ({ - ...trial, - id: trial.nct_id, - }))} - title="Trials" - variant="minimal" - /> - )} - </Section> - ); -}; +}): JSX.Element => ( + <Section mx={3}> + <Title description="" title={asset.name} variant="soft" /> + </Section> +); diff --git a/src/components/composite/config.ts b/src/components/composite/config.ts index 60f33d8..56a52e4 100644 --- a/src/components/composite/config.ts +++ b/src/components/composite/config.ts @@ -13,6 +13,7 @@ import { renderBoolean, renderChip, renderLabel, + renderMainTypography, renderPrimaryChip, renderPercent, renderSparkline, @@ -241,6 +242,7 @@ export const getAssetColumns = (): GridColDef[] => [ field: 'name', headerName: 'Asset or Target', width: 250, + renderCell: renderMainTypography, }, { field: 'trial_count', diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 7ab3585..9f958f4 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -3,6 +3,7 @@ import { ReactNode } from 'react'; import TrueIcon from '@mui/icons-material/Check'; import FalseIcon from '@mui/icons-material/Close'; +import Typography, { TypographyProps } from '@mui/joy/Typography'; import { GridRenderCellParams, GridValueFormatterParams, @@ -221,3 +222,15 @@ export const renderLabel = <T extends Record<string, unknown>>( return formatLabel(value); }; + +export const getRenderTypography = + <T extends Record<string, unknown>>(level: TypographyProps['level']) => + (params: GridRenderCellParams<T, string>): ReactNode => { + const { value } = params; + if (!value) { + return <span />; + } + return <Typography level={level}>{value}</Typography>; + }; + +export const renderMainTypography = getRenderTypography('title-md'); From d1df09f2d3ecd0bf00262ba049aaaeee010aaf77 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 13:45:44 -0700 Subject: [PATCH 07/19] update hard-coded urls --- src/app/core/dashboard/graph.tsx | 2 +- src/app/core/dashboard/over-time.tsx | 2 +- src/app/core/dashboard/summary.tsx | 2 +- src/components/composite/patents/client.tsx | 2 +- src/components/data/grid/formatters.tsx | 2 +- src/components/navigation/menu.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/core/dashboard/graph.tsx b/src/app/core/dashboard/graph.tsx index ef7b080..e5ed633 100644 --- a/src/app/core/dashboard/graph.tsx +++ b/src/app/core/dashboard/graph.tsx @@ -31,7 +31,7 @@ const fetchGraph = cache( ); export const PatentGraph = async ({ - pathname = '/core/patents', + pathname = '/core/dashboard', terms, ...args }: PatentSearchArgs & { pathname?: string }) => { diff --git a/src/app/core/dashboard/over-time.tsx b/src/app/core/dashboard/over-time.tsx index dcce4f7..e5ffaa3 100644 --- a/src/app/core/dashboard/over-time.tsx +++ b/src/app/core/dashboard/over-time.tsx @@ -35,7 +35,7 @@ const fetchReports = cache( ); export const OverTime = async ({ - pathname = '/core/patents', + pathname = '/core/dashboard', ...args }: PatentSearchArgs & { pathname?: string }) => { try { diff --git a/src/app/core/dashboard/summary.tsx b/src/app/core/dashboard/summary.tsx index 8e6c09e..3b3a3aa 100644 --- a/src/app/core/dashboard/summary.tsx +++ b/src/app/core/dashboard/summary.tsx @@ -44,7 +44,7 @@ const fetchSummaries = cache( // ); export const Summary = async ({ - pathname = '/core/patents', + pathname = '/core/dashboard', terms, ...args }: PatentSearchArgs & { pathname?: string }) => { diff --git a/src/components/composite/patents/client.tsx b/src/components/composite/patents/client.tsx index 8e642aa..a2ba8b7 100644 --- a/src/components/composite/patents/client.tsx +++ b/src/components/composite/patents/client.tsx @@ -40,7 +40,7 @@ const SimilarPatents = ({ patent }: { patent: Patent }): JSX.Element => ( ( <Link component={NextLink} - href={`/core/patents?terms=${s}`} + href={`/core/dashboard?terms=${s}`} target="_blank" > Search diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 9f958f4..d756da1 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -181,7 +181,7 @@ export const renderPrimaryChip = getRenderChip('primary'); export const renderChip = getRenderChip('neutral'); export const renderAssetCountChip = getRenderChip( 'primary', - (row: { name: string }) => `/core/patents?terms=${row.name}` + (row: { name: string }) => `/core/dashboard?terms=${row.name}` ); export const getRenderSparkline = diff --git a/src/components/navigation/menu.tsx b/src/components/navigation/menu.tsx index 83fb7f6..b9ffbdc 100644 --- a/src/components/navigation/menu.tsx +++ b/src/components/navigation/menu.tsx @@ -153,7 +153,7 @@ export const SideNav = () => { onKeyDown={() => setMenuIndex(null)} menu={ <Menu onClose={() => setMenuIndex(null)}> - <MenuItem {...itemProps} href="/core/patents"> + <MenuItem {...itemProps} href="/core/dashboard"> Patents Search </MenuItem> </Menu> From e0550376ca18d73e87a7646eb3696afc8ea52c3c Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 14:14:12 -0700 Subject: [PATCH 08/19] added sparkline to detail --- src/app/core/dashboard/asset.tsx | 1 + src/components/composite/assets/client.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index ecb570f..66426de 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -8,6 +8,7 @@ import 'server-only'; import { DataGrid } from '@/components/data/grid'; import { getAssetColumns } from '@/components/composite/config'; +// import { AssetDetail } from '@/components/composite/assets/client'; import { EntitySearchArgs } from '@/types/entities'; import { fetchAssets } from '../actions'; diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx index 00370a2..326ddf4 100644 --- a/src/components/composite/assets/client.tsx +++ b/src/components/composite/assets/client.tsx @@ -1,4 +1,6 @@ -'use server'; +'use client'; + +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; import { Section } from '@/components/layout/section'; import { Title } from '@/components/layout/title'; @@ -14,5 +16,14 @@ export const AssetDetail = <T extends Entity>({ }): JSX.Element => ( <Section mx={3}> <Title description="" title={asset.name} variant="soft" /> + <SparkLineChart + showHighlight + showTooltip + plotType="line" + colors={['blue']} + data={asset.activity} + height={200} + margin={{ top: 20, right: 20, bottom: 20, left: 20 }} + /> </Section> ); From fee1ab3c7aaf18de28e29638a1442775e7dcec04 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Tue, 19 Dec 2023 16:08:16 -0700 Subject: [PATCH 09/19] display approval data --- src/app/core/dashboard/asset.tsx | 7 ++++--- src/components/composite/config.ts | 10 ++++++++-- src/components/data/grid/formatters.tsx | 1 + src/components/data/grid/grid.tsx | 1 + src/types/entities.ts | 2 ++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index 66426de..c840e2a 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -8,8 +8,8 @@ import 'server-only'; import { DataGrid } from '@/components/data/grid'; import { getAssetColumns } from '@/components/composite/config'; -// import { AssetDetail } from '@/components/composite/assets/client'; -import { EntitySearchArgs } from '@/types/entities'; +import { AssetDetail } from '@/components/composite/assets/client'; +import { Entity, EntitySearchArgs } from '@/types/entities'; import { fetchAssets } from '../actions'; @@ -20,8 +20,9 @@ export const AssetList = async (args: EntitySearchArgs) => { return ( <Box height="100vh"> <DataGrid + checkboxSelection columns={columns} - // detailComponent={AssetDetail<Entity>} + detailComponent={AssetDetail<Entity>} detailHeight={800} rows={assets.map((asset) => ({ ...asset, diff --git a/src/components/composite/config.ts b/src/components/composite/config.ts index 56a52e4..c863d7b 100644 --- a/src/components/composite/config.ts +++ b/src/components/composite/config.ts @@ -244,16 +244,22 @@ export const getAssetColumns = (): GridColDef[] => [ width: 250, renderCell: renderMainTypography, }, + { + field: 'approval_count', + headerName: 'Approvals', + width: 100, + renderCell: renderAssetCountChip, + }, { field: 'trial_count', headerName: 'Trials', - width: 125, + width: 100, renderCell: renderAssetCountChip, }, { field: 'patent_count', headerName: 'Patents', - width: 125, + width: 100, renderCell: renderAssetCountChip, }, { diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index d756da1..6cebcc0 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -193,6 +193,7 @@ export const getRenderSparkline = } return ( <SparkLineChart + showHighlight plotType="line" colors={['blue']} data={value} diff --git a/src/components/data/grid/grid.tsx b/src/components/data/grid/grid.tsx index 04dcc4b..07ad7cf 100644 --- a/src/components/data/grid/grid.tsx +++ b/src/components/data/grid/grid.tsx @@ -13,6 +13,7 @@ import { GridColDef, GridToolbar } from '@mui/x-data-grid'; type DataGridVariant = 'standard' | 'minimal' | 'maximal'; type DataGridProps<T> = { + checkboxSelection?: MuiDataGridProps['checkboxSelection']; columns?: GridColDef[]; detailComponent?: ({ row }: { row: T }) => JSX.Element; detailHeight?: number; diff --git a/src/types/entities.ts b/src/types/entities.ts index 73e5fb8..7cb46ab 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -6,6 +6,8 @@ import { TrialSchema } from './trials'; export const EntitySchema = z.object({ activity: z.array(z.number()), + approval_count: z.number(), + is_approved: z.boolean(), last_status: z.string(), last_updated: z.union([z.number(), z.null()]), name: z.string(), From c0f3f4f8709f8d25d033520b3f95bcc05d80f363 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 14:09:02 -0700 Subject: [PATCH 10/19] cleanup --- src/app/core/dashboard/content.tsx | 18 +++++++++--------- src/components/composite/config.ts | 26 +------------------------- src/types/patents.ts | 4 ---- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/src/app/core/dashboard/content.tsx b/src/app/core/dashboard/content.tsx index 9f2f62e..4613ee5 100644 --- a/src/app/core/dashboard/content.tsx +++ b/src/app/core/dashboard/content.tsx @@ -13,7 +13,7 @@ import { PatentSearchArgs } from '@/types/patents'; import { AssetList } from './asset'; import { PatentList } from './patent'; -import { PatentGraph } from './graph'; +// import { PatentGraph } from './graph'; import { OverTime } from './over-time'; import { Summary } from './summary'; import { TrialList } from './trials'; @@ -61,14 +61,14 @@ export const Content = (args: PatentSearchArgs) => { </Suspense> ), }, - { - label: 'Graph', - panel: ( - <Suspense fallback={<Skeleton />}> - <PatentGraph {...args} /> - </Suspense> - ), - }, + // { + // label: 'Graph', + // panel: ( + // <Suspense fallback={<Skeleton />}> + // <PatentGraph {...args} /> + // </Suspense> + // ), + // }, ]; return ( <Box sx={getStyles}> diff --git a/src/components/composite/config.ts b/src/components/composite/config.ts index c863d7b..ce06ce7 100644 --- a/src/components/composite/config.ts +++ b/src/components/composite/config.ts @@ -5,12 +5,10 @@ import 'server-only'; import { formatBlank, - formatDate, formatName, formatNumber, formatYear, renderAssetCountChip, - renderBoolean, renderChip, renderLabel, renderMainTypography, @@ -111,28 +109,6 @@ export const getPatentColumns = (): GridColDef[] => [ width: 100, description: 'Similarity to exemplar patent.', }, - { - field: 'is_approved', - headerName: 'Approved', - width: 75, - renderCell: renderBoolean, - }, - { - field: 'max_trial_phase', - headerName: 'CT Phase', - width: 100, - }, - { - field: 'last_trial_status', - headerName: 'CT Status', - width: 125, - }, - { - field: 'last_trial_update', - headerName: 'Last CT Update', - valueFormatter: formatDate, - width: 125, - }, { field: 'priority_date', headerName: 'Priority Year', @@ -240,7 +216,7 @@ export const getTrialColumns = (): GridColDef[] => [ export const getAssetColumns = (): GridColDef[] => [ { field: 'name', - headerName: 'Asset or Target', + headerName: 'Asset, Class or Target', width: 250, renderCell: renderMainTypography, }, diff --git a/src/types/patents.ts b/src/types/patents.ts index 3e4fd03..e198a08 100644 --- a/src/types/patents.ts +++ b/src/types/patents.ts @@ -6,23 +6,19 @@ export const PatentSchema = z.object({ title: z.string(), abstract: z.string(), adj_patent_years: z.number(), - approval_dates: z.optional(z.array(z.string())), assignees: z.array(z.string()), attributes: z.array(z.string()), availability_likelihood: z.string(), availability_explanation: z.string(), biologics: z.array(z.string()), - brand_name: z.union([z.string(), z.null()]), compounds: z.array(z.string()), devices: z.array(z.string()), diseases: z.array(z.string()), exemplar_similarity: z.number(), - generic_name: z.union([z.string(), z.null()]), // genes: z.array(z.string()), indications: z.optional(z.union([z.array(z.string()), z.null()])), inventors: z.array(z.string()), ipc_codes: z.array(z.string()), - is_approved: z.optional(z.boolean()), last_trial_status: z.optional(z.union([z.string(), z.null()])), last_trial_update: z.optional(z.union([z.string(), z.null()])), max_trial_phase: z.optional(z.union([z.string(), z.null()])), From d6acf0970dfab35b65c4e23cd1ab881932b264dd Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 16:51:36 -0700 Subject: [PATCH 11/19] new fields, display tweaks --- src/components/composite/config.ts | 24 ++++++++++++++++++------ src/components/data/grid/formatters.tsx | 12 ++++++------ src/types/entities.ts | 5 ++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/components/composite/config.ts b/src/components/composite/config.ts index ce06ce7..dbb3742 100644 --- a/src/components/composite/config.ts +++ b/src/components/composite/config.ts @@ -217,27 +217,33 @@ export const getAssetColumns = (): GridColDef[] => [ { field: 'name', headerName: 'Asset, Class or Target', - width: 250, + width: 300, renderCell: renderMainTypography, }, { field: 'approval_count', headerName: 'Approvals', - width: 100, + width: 85, renderCell: renderAssetCountChip, }, { field: 'trial_count', headerName: 'Trials', - width: 100, + width: 85, renderCell: renderAssetCountChip, }, { field: 'patent_count', headerName: 'Patents', - width: 100, + width: 85, renderCell: renderAssetCountChip, }, + { + field: 'owner_count', + headerName: 'Owners', + width: 85, + renderCell: renderChip, + }, { field: 'activity', headerName: 'Activity', @@ -246,8 +252,14 @@ export const getAssetColumns = (): GridColDef[] => [ }, { field: 'last_updated', - headerName: 'Last Update', - width: 125, + headerName: 'Updated', + width: 85, + }, + { + field: 'maybe_available_count', + headerName: 'Available?', + renderCell: renderChip, + width: 100, }, { field: 'max_phase', diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 6cebcc0..62a4679 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -128,8 +128,8 @@ export const formatYear = getFormatDate('yyyy'); /** * Render boolean */ -export const renderBoolean = ( - params: GridRenderCellParams<string[]> +export const renderBoolean = <T extends Record<string, unknown>>( + params: GridRenderCellParams<T> ): ReactNode => params.value ? ( <TrueIcon sx={{ m: 'auto' }} /> @@ -160,16 +160,15 @@ export const getRenderChip = ) => (params: GridRenderCellParams<T, string | number>): ReactNode => { const { value, row } = params; + if (value === null || typeof value === 'undefined') { + return formatBlank(); + } if (typeof value !== 'string' && typeof value !== 'number') { return <>{JSON.stringify(value)}</>; } const href = getUrl(row); - if (typeof value !== 'number' && !value) { - return <span />; - } - return ( <Chip color={color} href={href}> {formatLabel(value)} @@ -178,6 +177,7 @@ export const getRenderChip = }; export const renderPrimaryChip = getRenderChip('primary'); +export const renderWarningChip = getRenderChip('warning'); export const renderChip = getRenderChip('neutral'); export const renderAssetCountChip = getRenderChip( 'primary', diff --git a/src/types/entities.ts b/src/types/entities.ts index 7cb46ab..1c52210 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -10,8 +10,11 @@ export const EntitySchema = z.object({ is_approved: z.boolean(), last_status: z.string(), last_updated: z.union([z.number(), z.null()]), - name: z.string(), + maybe_available_count: z.number(), max_phase: z.string(), + name: z.string(), + owners: z.array(z.string()), + owner_count: z.number(), patents: z.array(PatentSchema), patent_count: z.number(), record_count: z.number(), From 3574cfa040da4948a428ba5496895ce13ed362d5 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 17:41:36 -0700 Subject: [PATCH 12/19] modal detail on datagrid --- src/app/core/dashboard/asset.tsx | 7 +- src/app/core/dashboard/patent.tsx | 2 +- src/app/core/dashboard/trials.tsx | 2 +- src/components/composite/assets/client.tsx | 66 +++-- src/components/composite/assets/config.tsx | 82 ++++++ src/components/composite/config.ts | 276 -------------------- src/components/composite/patents/client.tsx | 9 +- src/components/composite/patents/config.tsx | 109 ++++++++ src/components/composite/trials/config.ts | 111 ++++++++ src/components/data/grid/formatters.tsx | 18 +- src/components/data/grid/grid.tsx | 1 + src/components/navigation/modal.tsx | 38 +++ 12 files changed, 409 insertions(+), 312 deletions(-) create mode 100644 src/components/composite/assets/config.tsx delete mode 100644 src/components/composite/config.ts create mode 100644 src/components/composite/patents/config.tsx create mode 100644 src/components/composite/trials/config.ts create mode 100644 src/components/navigation/modal.tsx diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index c840e2a..a537f67 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -6,10 +6,9 @@ import Typography from '@mui/joy/Typography'; import WarningIcon from '@mui/icons-material/Warning'; import 'server-only'; +import { getAssetColumns } from '@/components/composite/assets/config'; import { DataGrid } from '@/components/data/grid'; -import { getAssetColumns } from '@/components/composite/config'; -import { AssetDetail } from '@/components/composite/assets/client'; -import { Entity, EntitySearchArgs } from '@/types/entities'; +import { EntitySearchArgs } from '@/types/entities'; import { fetchAssets } from '../actions'; @@ -22,8 +21,6 @@ export const AssetList = async (args: EntitySearchArgs) => { <DataGrid checkboxSelection columns={columns} - detailComponent={AssetDetail<Entity>} - detailHeight={800} rows={assets.map((asset) => ({ ...asset, id: asset.name, diff --git a/src/app/core/dashboard/patent.tsx b/src/app/core/dashboard/patent.tsx index dd149c5..32bce83 100644 --- a/src/app/core/dashboard/patent.tsx +++ b/src/app/core/dashboard/patent.tsx @@ -6,7 +6,7 @@ import Typography from '@mui/joy/Typography'; import WarningIcon from '@mui/icons-material/Warning'; import 'server-only'; -import { getPatentColumns } from '@/components/composite/config'; +import { getPatentColumns } from '@/components/composite/patents/config'; import { PatentDetail } from '@/components/composite/patents/client'; import { DataGrid } from '@/components/data/grid'; import { Patent, PatentSearchArgs } from '@/types/patents'; diff --git a/src/app/core/dashboard/trials.tsx b/src/app/core/dashboard/trials.tsx index 7022b36..9266c70 100644 --- a/src/app/core/dashboard/trials.tsx +++ b/src/app/core/dashboard/trials.tsx @@ -6,7 +6,7 @@ import Typography from '@mui/joy/Typography'; import WarningIcon from '@mui/icons-material/Warning'; import 'server-only'; -import { getTrialColumns } from '@/components/composite/config'; +import { getTrialColumns } from '@/components/composite/trials/config'; import { TrialDetail } from '@/components/composite/trials/client'; import { DataGrid } from '@/components/data/grid'; import { Trial, TrialSearchArgs } from '@/types/trials'; diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx index 326ddf4..b2243f5 100644 --- a/src/components/composite/assets/client.tsx +++ b/src/components/composite/assets/client.tsx @@ -1,10 +1,17 @@ 'use client'; -import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; +import Box from '@mui/joy/Box'; +import { GridRenderCellParams } from '@mui/x-data-grid/models/params'; -import { Section } from '@/components/layout/section'; -import { Title } from '@/components/layout/title'; import { Entity } from '@/types/entities'; +import { Modal } from '@/components/navigation/modal'; +import { DataGrid } from '@/components/data/grid'; +import { Patent } from '@/types/patents'; +import { Trial } from '@/types/trials'; + +import { getPatentColumns } from '../patents/config'; +import { getStyles } from '../styles'; +import { getTrialColumns } from '../trials/config'; /** * Detail content panel for patents grid @@ -13,17 +20,46 @@ export const AssetDetail = <T extends Entity>({ row: asset, }: { row: T; +}): JSX.Element => { + const patentColumns = getPatentColumns(); + const trialColumns = getTrialColumns(); + return ( + <Box sx={getStyles}> + {asset.patents.length > 0 && ( + <DataGrid + columns={patentColumns} + getRowId={(row: Patent) => row.publication_number} + rows={asset.patents} + title="Patents" + variant="minimal" + /> + )} + {asset.trials.length > 0 && ( + <DataGrid + columns={trialColumns} + getRowId={(row: Trial) => row.nct_id} + rows={asset.trials} + title="Trials" + variant="minimal" + /> + )} + </Box> + ); +}; + +export const AssetModal = <T extends Entity>({ + row: asset, +}: { + row: T; }): JSX.Element => ( - <Section mx={3}> - <Title description="" title={asset.name} variant="soft" /> - <SparkLineChart - showHighlight - showTooltip - plotType="line" - colors={['blue']} - data={asset.activity} - height={200} - margin={{ top: 20, right: 20, bottom: 20, left: 20 }} - /> - </Section> + <Modal title={asset.name}> + <AssetDetail row={asset} /> + </Modal> ); + +export const renderAssetModal = <T extends Entity>( + params: GridRenderCellParams<T> +): JSX.Element => { + const { row } = params; + return <AssetModal row={row} />; +}; diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx new file mode 100644 index 0000000..38462a9 --- /dev/null +++ b/src/components/composite/assets/config.tsx @@ -0,0 +1,82 @@ +'use server'; + +import { GridColDef } from '@mui/x-data-grid/models/colDef'; +import 'server-only'; + +import { + renderAssetCountChip, + renderChip, + renderMainTypography, + renderSparkline, +} from '@/components/data/grid'; + +import { renderAssetModal } from './client'; + +export const getAssetColumns = (): GridColDef[] => [ + { + field: 'name', + headerName: 'Asset, Class or Target', + width: 300, + renderCell: renderMainTypography, + }, + { + field: 'approval_count', + headerName: 'Approvals', + width: 85, + renderCell: renderAssetCountChip, + }, + { + field: 'trial_count', + headerName: 'Trials', + width: 85, + renderCell: renderAssetCountChip, + }, + { + field: 'patent_count', + headerName: 'Patents', + width: 85, + renderCell: renderAssetCountChip, + }, + { + field: 'owner_count', + headerName: 'Owners', + width: 85, + renderCell: renderChip, + }, + { + field: 'activity', + headerName: 'Activity', + width: 125, + renderCell: renderSparkline, + }, + { + field: 'last_updated', + headerName: 'Updated', + width: 85, + }, + { + field: 'maybe_available_count', + headerName: 'Available?', + renderCell: renderChip, + width: 100, + }, + { + field: 'max_phase', + headerName: 'Max Phase', + width: 125, + renderCell: renderChip, + }, + { + field: 'last_status', + headerName: 'Last Status', + width: 125, + renderCell: renderChip, + }, + { + field: 'actions', + headerName: 'Actions', + width: 100, + type: 'actions', + renderCell: renderAssetModal, + }, +]; diff --git a/src/components/composite/config.ts b/src/components/composite/config.ts deleted file mode 100644 index dbb3742..0000000 --- a/src/components/composite/config.ts +++ /dev/null @@ -1,276 +0,0 @@ -'use server'; - -import { GridColDef } from '@mui/x-data-grid/models/colDef'; -import 'server-only'; - -import { - formatBlank, - formatName, - formatNumber, - formatYear, - renderAssetCountChip, - renderChip, - renderLabel, - renderMainTypography, - renderPrimaryChip, - renderPercent, - renderSparkline, - unencodeHtml, -} from '@/components/data/grid'; - -import { - getAvailabilityClass, - getDropoutScoresClass, - getPatentYearsClass, - getRepurposeScoreClass, - getScoresClass, - getTolerantScoresClass, -} from './styles'; - -export const getPatentColumns = (): GridColDef[] => [ - { field: 'publication_number', headerName: 'Pub #', width: 170 }, - { - field: 'title', - headerName: 'Title', - width: 500, - valueFormatter: unencodeHtml, - }, - { - field: 'score', - headerName: 'Overall', - width: 85, - valueFormatter: formatNumber, - cellClassName: getTolerantScoresClass, - description: 'Overall score', - }, - { - field: 'suitability_score', - headerName: 'Suitability', - width: 85, - valueFormatter: formatNumber, - cellClassName: getScoresClass, - description: - 'Suitability of patent, in terms of patent type (CoM vs MoU), patented thing (compound > device) and patent years remaining.', - }, - { - field: 'patent_years', - headerName: 'Yrs Left', - width: 75, - description: 'Patent years remaining.', - cellClassName: getPatentYearsClass, - }, - // { - // field: 'adj_patent_years', - // headerName: 'Adj Yrs⚠️', - // width: 75, - // description: '**FAKE** Adjusted patent years remaining.', - // cellClassName: getPatentYearsClass, - // }, - { - field: 'availability_likelihood', - headerName: 'Est. Avail', - width: 100, - valueFormatter: formatName, - cellClassName: getAvailabilityClass, - description: 'Likehood of patent being available.', - }, - { - field: 'assignees', - headerName: 'Assignees', - valueFormatter: formatName, - width: 200, - }, - { - field: 'probability_of_success', - headerName: 'PoS', - width: 85, - valueFormatter: formatBlank, - // cellClassName: getScoresClass, - description: '**FAKE PLACEHOLDER**!! Estimated PoS.', - }, - { - field: 'reformulation_score', - headerName: 'Reformulation⚠️', - width: 100, - valueFormatter: renderPercent, - // cellClassName: getScoresClass, - description: '**FAKE PLACEHOLDER**!! Esimated reformulation potential.', - }, - { - field: 'search_rank', - headerName: 'Relevance', - valueFormatter: formatNumber, - width: 100, - }, - { - field: 'exemplar_similarity', - headerName: 'Similarity', - valueFormatter: formatNumber, - width: 100, - description: 'Similarity to exemplar patent.', - }, - { - field: 'priority_date', - headerName: 'Priority Year', - valueFormatter: formatYear, - width: 125, - }, -]; - -export const getTrialColumns = (): GridColDef[] => [ - { field: 'nct_id', headerName: 'Nct Id', width: 135 }, - { - field: 'title', - headerName: 'Title', - width: 500, - }, - { - field: 'intervention', - headerName: 'Intervention', - renderCell: renderChip, - width: 200, - }, - { - field: 'condition', - headerName: 'Condition', - renderCell: renderChip, - width: 175, - }, - { - field: 'sponsor', - headerName: 'Sponsor', - width: 175, - valueFormatter: formatName, - }, - { - field: 'start_date', - headerName: 'Start', - width: 75, - valueFormatter: formatYear, - }, - { - field: 'end_date', - headerName: 'End', - width: 75, - valueFormatter: formatYear, - }, - { - field: 'phase', - headerName: 'Phase', - renderCell: renderChip, - width: 100, - }, - { - field: 'status', - headerName: 'Status', - renderCell: renderPrimaryChip, - width: 125, - }, - - { - field: 'dropout_percent', - headerName: 'Dropout %', - width: 100, - valueFormatter: renderPercent, - cellClassName: getDropoutScoresClass, - description: 'Dropout % = Dropouts / Enrollment', - }, - { - field: 'termination_reason', - headerName: 'Term. Reason', - width: 150, - }, - { - field: 'reformulation_score', - headerName: 'Repurpose⚠️', - width: 150, - valueFormatter: formatNumber, - cellClassName: getRepurposeScoreClass, - description: '**FAKE PLACEHOLDER**!! Esimated repurpose potential.', - }, - { - field: 'design', - headerName: 'Design', - width: 150, - valueFormatter: renderLabel, - }, - { - field: 'duration', - headerName: 'Duration', - width: 100, - valueFormatter: formatNumber, - }, - { - field: 'max_timeframe', - headerName: 'Timeframe', - width: 100, - }, - { - field: 'enrollment', - headerName: 'Enrollment', - width: 100, - valueFormatter: formatNumber, - }, -]; - -export const getAssetColumns = (): GridColDef[] => [ - { - field: 'name', - headerName: 'Asset, Class or Target', - width: 300, - renderCell: renderMainTypography, - }, - { - field: 'approval_count', - headerName: 'Approvals', - width: 85, - renderCell: renderAssetCountChip, - }, - { - field: 'trial_count', - headerName: 'Trials', - width: 85, - renderCell: renderAssetCountChip, - }, - { - field: 'patent_count', - headerName: 'Patents', - width: 85, - renderCell: renderAssetCountChip, - }, - { - field: 'owner_count', - headerName: 'Owners', - width: 85, - renderCell: renderChip, - }, - { - field: 'activity', - headerName: 'Activity', - width: 125, - renderCell: renderSparkline, - }, - { - field: 'last_updated', - headerName: 'Updated', - width: 85, - }, - { - field: 'maybe_available_count', - headerName: 'Available?', - renderCell: renderChip, - width: 100, - }, - { - field: 'max_phase', - headerName: 'Max Phase', - width: 125, - renderCell: renderChip, - }, - { - field: 'last_status', - headerName: 'Last Status', - width: 125, - renderCell: renderChip, - }, -]; diff --git a/src/components/composite/patents/client.tsx b/src/components/composite/patents/client.tsx index a2ba8b7..4fa8622 100644 --- a/src/components/composite/patents/client.tsx +++ b/src/components/composite/patents/client.tsx @@ -72,11 +72,6 @@ export const PatentDetail = <T extends Patent>({ 'mechanisms', ]; const pathname = usePathname(); - const approvalInfo = patent.approval_dates - ? `\n\nApproved ${patent.approval_dates[0]}} for indication ${ - patent.indications?.[0] || '(unknown)' - } (${patent.brand_name}/${patent.generic_name}).` - : ''; const trialInfo = patent.last_trial_status ? `\n\nLast trial update: ${patent.last_trial_status} on ${ patent.last_trial_update @@ -85,9 +80,7 @@ export const PatentDetail = <T extends Patent>({ return ( <Section mx={3}> <Title - description={`${unescape( - patent.abstract - )}${approvalInfo}${trialInfo}`} + description={`${unescape(patent.abstract)}${trialInfo}`} link={{ label: patent.publication_number, url: patent.url }} title={unescape(patent.title)} variant="soft" diff --git a/src/components/composite/patents/config.tsx b/src/components/composite/patents/config.tsx new file mode 100644 index 0000000..e44d782 --- /dev/null +++ b/src/components/composite/patents/config.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { GridColDef } from '@mui/x-data-grid/models/colDef'; + +import { + formatBlank, + formatName, + formatNumber, + formatYear, + renderPercent, + unencodeHtml, +} from '@/components/data/grid'; + +import { + getAvailabilityClass, + getPatentYearsClass, + getScoresClass, + getTolerantScoresClass, +} from '../styles'; + +export const getPatentColumns = (): GridColDef[] => [ + { field: 'publication_number', headerName: 'Pub #', width: 170 }, + { + field: 'title', + headerName: 'Title', + width: 500, + valueFormatter: unencodeHtml, + }, + { + field: 'score', + headerName: 'Overall', + width: 85, + valueFormatter: formatNumber, + cellClassName: getTolerantScoresClass, + description: 'Overall score', + }, + { + field: 'suitability_score', + headerName: 'Suitability', + width: 85, + valueFormatter: formatNumber, + cellClassName: getScoresClass, + description: + 'Suitability of patent, in terms of patent type (CoM vs MoU), patented thing (compound > device) and patent years remaining.', + }, + { + field: 'patent_years', + headerName: 'Yrs Left', + width: 75, + description: 'Patent years remaining.', + cellClassName: getPatentYearsClass, + }, + // { + // field: 'adj_patent_years', + // headerName: 'Adj Yrs⚠️', + // width: 75, + // description: '**FAKE** Adjusted patent years remaining.', + // cellClassName: getPatentYearsClass, + // }, + { + field: 'availability_likelihood', + headerName: 'Est. Avail', + width: 100, + valueFormatter: formatName, + cellClassName: getAvailabilityClass, + description: 'Likehood of patent being available.', + }, + { + field: 'assignees', + headerName: 'Assignees', + valueFormatter: formatName, + width: 200, + }, + { + field: 'probability_of_success', + headerName: 'PoS', + width: 85, + valueFormatter: formatBlank, + // cellClassName: getScoresClass, + description: '**FAKE PLACEHOLDER**!! Estimated PoS.', + }, + { + field: 'reformulation_score', + headerName: 'Reformulation⚠️', + width: 100, + valueFormatter: renderPercent, + // cellClassName: getScoresClass, + description: '**FAKE PLACEHOLDER**!! Esimated reformulation potential.', + }, + { + field: 'search_rank', + headerName: 'Relevance', + valueFormatter: formatNumber, + width: 100, + }, + { + field: 'exemplar_similarity', + headerName: 'Similarity', + valueFormatter: formatNumber, + width: 100, + description: 'Similarity to exemplar patent.', + }, + { + field: 'priority_date', + headerName: 'Priority Year', + valueFormatter: formatYear, + width: 125, + }, +]; diff --git a/src/components/composite/trials/config.ts b/src/components/composite/trials/config.ts new file mode 100644 index 0000000..35b3c7c --- /dev/null +++ b/src/components/composite/trials/config.ts @@ -0,0 +1,111 @@ +'use client'; + +import { GridColDef } from '@mui/x-data-grid/models/colDef'; + +import { + formatName, + formatNumber, + formatYear, + renderChip, + renderLabel, + renderPrimaryChip, + renderPercent, +} from '@/components/data/grid'; + +import { getDropoutScoresClass, getRepurposeScoreClass } from '../styles'; + +export const getTrialColumns = (): GridColDef[] => [ + { field: 'nct_id', headerName: 'Nct Id', width: 135 }, + { + field: 'title', + headerName: 'Title', + width: 500, + }, + { + field: 'intervention', + headerName: 'Intervention', + renderCell: renderChip, + width: 200, + }, + { + field: 'condition', + headerName: 'Condition', + renderCell: renderChip, + width: 175, + }, + { + field: 'sponsor', + headerName: 'Sponsor', + width: 175, + valueFormatter: formatName, + }, + { + field: 'start_date', + headerName: 'Start', + width: 75, + valueFormatter: formatYear, + }, + { + field: 'end_date', + headerName: 'End', + width: 75, + valueFormatter: formatYear, + }, + { + field: 'phase', + headerName: 'Phase', + renderCell: renderChip, + width: 100, + }, + { + field: 'status', + headerName: 'Status', + renderCell: renderPrimaryChip, + width: 125, + }, + + { + field: 'dropout_percent', + headerName: 'Dropout %', + width: 100, + valueFormatter: renderPercent, + cellClassName: getDropoutScoresClass, + description: 'Dropout % = Dropouts / Enrollment', + }, + { + field: 'termination_reason', + headerName: 'Term. Reason', + width: 150, + }, + { + field: 'reformulation_score', + headerName: 'Repurpose⚠️', + width: 150, + valueFormatter: formatNumber, + cellClassName: getRepurposeScoreClass, + description: '**FAKE PLACEHOLDER**!! Esimated repurpose potential.', + }, + { + field: 'design', + headerName: 'Design', + width: 150, + valueFormatter: renderLabel, + }, + { + field: 'duration', + headerName: 'Duration', + width: 100, + valueFormatter: formatNumber, + }, + { + field: 'max_timeframe', + headerName: 'Timeframe', + width: 100, + }, + { + field: 'enrollment', + headerName: 'Enrollment', + width: 100, + valueFormatter: formatNumber, + }, +]; diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 62a4679..43f0d11 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -32,7 +32,8 @@ export const formatName = <T extends Record<string, unknown>>( } if (typeof value !== 'string') { - throw new Error(`Expected string, got ${typeof value}`); + console.error(`Expected string, got ${typeof value}`); + return ''; } return title(value); @@ -56,7 +57,8 @@ export const formatNumber = <T extends Record<string, unknown>>( } if (typeof value !== 'number') { - throw new Error(`Expected number, got ${typeof value}`); + console.error(`Expected number, got ${typeof value}`); + return formatBlank(); } return `${parseFloat((value as number).toPrecision(2))}`; @@ -71,7 +73,8 @@ export const renderPercent = <T extends Record<string, unknown>>( const { value } = params; if (typeof value !== 'number') { - throw new Error(`Expected number, got ${typeof value}`); + console.error(`Expected number, got ${typeof value}`); + return formatBlank(); } return formatPercent(value); @@ -92,7 +95,8 @@ export const unencodeHtml = <T extends Record<string, unknown>>( } if (typeof value !== 'string') { - throw new Error(`Expected string, got ${typeof value}`); + console.error(`Expected string, got ${typeof value}`); + return ''; } return unescape(value); @@ -144,7 +148,8 @@ export const renderList = ( params: GridRenderCellParams<string[]> ): ReactNode => { if (!Array.isArray(params.value)) { - throw new Error(`Expected list, got ${typeof params.value}`); + console.error(`Expected list, got ${typeof params.value}`); + return formatBlank(); } return formatChips({ isWrappable: false, items: params.value as string[] }); @@ -218,7 +223,8 @@ export const renderLabel = <T extends Record<string, unknown>>( } if (typeof value !== 'string') { - throw new Error(`Expected string, got ${typeof value}`); + console.error(`Expected string, got ${typeof value}`); + return formatBlank(); } return formatLabel(value); diff --git a/src/components/data/grid/grid.tsx b/src/components/data/grid/grid.tsx index 07ad7cf..cd6eff4 100644 --- a/src/components/data/grid/grid.tsx +++ b/src/components/data/grid/grid.tsx @@ -17,6 +17,7 @@ type DataGridProps<T> = { columns?: GridColDef[]; detailComponent?: ({ row }: { row: T }) => JSX.Element; detailHeight?: number; + getRowId?: MuiDataGridProps['getRowId']; initialState?: MuiDataGridProps['initialState']; isLoading?: MuiDataGridProps['loading']; rows: MuiDataGridProps['rows']; diff --git a/src/components/navigation/modal.tsx b/src/components/navigation/modal.tsx new file mode 100644 index 0000000..a6b4d01 --- /dev/null +++ b/src/components/navigation/modal.tsx @@ -0,0 +1,38 @@ +import JoyModal from '@mui/joy/Modal'; +import ModalClose from '@mui/joy/ModalClose'; +import ModalDialog from '@mui/joy/ModalDialog'; +import ModalOverflow from '@mui/joy/ModalOverflow'; +import Typography from '@mui/joy/Typography'; +import LaunchIcon from '@mui/icons-material/Launch'; +import React from 'react'; +import IconButton from '@mui/joy/IconButton'; + +type ModalProps = { + children: React.ReactNode; + title: string; +}; + +export const Modal = ({ children, title }: ModalProps) => { + const [isOpen, setIsOpen] = React.useState<boolean>(false); + return ( + <> + <IconButton onClick={() => setIsOpen(true)}> + <LaunchIcon /> + </IconButton> + <JoyModal open={isOpen} onClose={() => setIsOpen(false)}> + <ModalOverflow> + <ModalDialog + aria-labelledby="modal-dialog-overflow" + layout="fullscreen" + > + <ModalClose /> + <Typography id="modal-dialog-overflow" level="h1"> + {title} + </Typography> + {children} + </ModalDialog> + </ModalOverflow> + </JoyModal> + </> + ); +}; From 8efeb669a0264a5b60aa3861534c50271c32a17c Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:05:55 -0700 Subject: [PATCH 13/19] show modal on chip click --- src/app/core/dashboard/asset.tsx | 1 + src/components/composite/assets/client.tsx | 78 ++++++++++++++++++---- src/components/composite/assets/config.tsx | 15 ++--- src/components/data/grid/grid.tsx | 1 + src/components/navigation/modal.tsx | 20 ++++-- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/app/core/dashboard/asset.tsx b/src/app/core/dashboard/asset.tsx index a537f67..e76d242 100644 --- a/src/app/core/dashboard/asset.tsx +++ b/src/app/core/dashboard/asset.tsx @@ -20,6 +20,7 @@ export const AssetList = async (args: EntitySearchArgs) => { <Box height="100vh"> <DataGrid checkboxSelection + disableRowSelectionOnClick columns={columns} rows={assets.map((asset) => ({ ...asset, diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx index b2243f5..1960843 100644 --- a/src/components/composite/assets/client.tsx +++ b/src/components/composite/assets/client.tsx @@ -3,9 +3,11 @@ import Box from '@mui/joy/Box'; import { GridRenderCellParams } from '@mui/x-data-grid/models/params'; -import { Entity } from '@/types/entities'; -import { Modal } from '@/components/navigation/modal'; +import { Chip } from '@/components/data/chip'; +import { formatLabel } from '@/utils/string'; +import { Modal, ModalButtonElementProps } from '@/components/navigation/modal'; import { DataGrid } from '@/components/data/grid'; +import { Entity } from '@/types/entities'; import { Patent } from '@/types/patents'; import { Trial } from '@/types/trials'; @@ -16,13 +18,12 @@ import { getTrialColumns } from '../trials/config'; /** * Detail content panel for patents grid */ -export const AssetDetail = <T extends Entity>({ +export const PatentDetail = <T extends Entity>({ row: asset, }: { row: T; }): JSX.Element => { const patentColumns = getPatentColumns(); - const trialColumns = getTrialColumns(); return ( <Box sx={getStyles}> {asset.patents.length > 0 && ( @@ -34,6 +35,21 @@ export const AssetDetail = <T extends Entity>({ variant="minimal" /> )} + </Box> + ); +}; + +/** + * Detail content panel for patents grid + */ +export const TrialDetail = <T extends Entity>({ + row: asset, +}: { + row: T; +}): JSX.Element => { + const trialColumns = getTrialColumns(); + return ( + <Box sx={getStyles}> {asset.trials.length > 0 && ( <DataGrid columns={trialColumns} @@ -47,19 +63,53 @@ export const AssetDetail = <T extends Entity>({ ); }; -export const AssetModal = <T extends Entity>({ - row: asset, -}: { +const getButtonElement = (value: number) => { + const ButtonElement = ({ onClick }: ModalButtonElementProps) => ( + <Chip color="primary" onClick={onClick}> + {formatLabel(value)} + </Chip> + ); + return ButtonElement; +}; + +type DocumentModalProps<T extends Entity> = { + buttonElement: (props: ModalButtonElementProps) => React.ReactNode; row: T; -}): JSX.Element => ( - <Modal title={asset.name}> - <AssetDetail row={asset} /> +}; + +export const TrialModal = <T extends Entity>({ + buttonElement, + row: trial, +}: DocumentModalProps<T>): JSX.Element => ( + <Modal buttonElement={buttonElement} title={trial.name}> + <TrialDetail row={trial} /> </Modal> ); -export const renderAssetModal = <T extends Entity>( - params: GridRenderCellParams<T> +export const PatentModal = <T extends Entity>({ + row: patent, + buttonElement, +}: DocumentModalProps<T>): JSX.Element => ( + <Modal buttonElement={buttonElement} title={patent.name}> + <PatentDetail row={patent} /> + </Modal> +); + +export const renderTrialModal = <T extends Entity>( + params: GridRenderCellParams<T, number> +): JSX.Element => { + const { row, value } = params; + if (typeof value !== 'number') { + return <span />; + } + return <TrialModal buttonElement={getButtonElement(value)} row={row} />; +}; +export const renderPatentModal = <T extends Entity>( + params: GridRenderCellParams<T, number> ): JSX.Element => { - const { row } = params; - return <AssetModal row={row} />; + const { row, value } = params; + if (typeof value !== 'number') { + return <span />; + } + return <PatentModal buttonElement={getButtonElement(value)} row={row} />; }; diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index 38462a9..d15a942 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -10,7 +10,7 @@ import { renderSparkline, } from '@/components/data/grid'; -import { renderAssetModal } from './client'; +import { renderPatentModal, renderTrialModal } from './client'; export const getAssetColumns = (): GridColDef[] => [ { @@ -29,13 +29,15 @@ export const getAssetColumns = (): GridColDef[] => [ field: 'trial_count', headerName: 'Trials', width: 85, - renderCell: renderAssetCountChip, + type: 'actions', + renderCell: renderTrialModal, }, { field: 'patent_count', headerName: 'Patents', width: 85, - renderCell: renderAssetCountChip, + type: 'actions', + renderCell: renderPatentModal, }, { field: 'owner_count', @@ -72,11 +74,4 @@ export const getAssetColumns = (): GridColDef[] => [ width: 125, renderCell: renderChip, }, - { - field: 'actions', - headerName: 'Actions', - width: 100, - type: 'actions', - renderCell: renderAssetModal, - }, ]; diff --git a/src/components/data/grid/grid.tsx b/src/components/data/grid/grid.tsx index cd6eff4..03d34fa 100644 --- a/src/components/data/grid/grid.tsx +++ b/src/components/data/grid/grid.tsx @@ -17,6 +17,7 @@ type DataGridProps<T> = { columns?: GridColDef[]; detailComponent?: ({ row }: { row: T }) => JSX.Element; detailHeight?: number; + disableRowSelectionOnClick?: MuiDataGridProps['disableRowSelectionOnClick']; getRowId?: MuiDataGridProps['getRowId']; initialState?: MuiDataGridProps['initialState']; isLoading?: MuiDataGridProps['loading']; diff --git a/src/components/navigation/modal.tsx b/src/components/navigation/modal.tsx index a6b4d01..1b1f2b2 100644 --- a/src/components/navigation/modal.tsx +++ b/src/components/navigation/modal.tsx @@ -5,20 +5,30 @@ import ModalOverflow from '@mui/joy/ModalOverflow'; import Typography from '@mui/joy/Typography'; import LaunchIcon from '@mui/icons-material/Launch'; import React from 'react'; -import IconButton from '@mui/joy/IconButton'; +import IconButton, { IconButtonProps } from '@mui/joy/IconButton'; + +export type ModalButtonElementProps = { + onClick: IconButtonProps['onClick']; +}; type ModalProps = { + buttonElement?: (props: ModalButtonElementProps) => React.ReactNode; children: React.ReactNode; title: string; }; -export const Modal = ({ children, title }: ModalProps) => { +const DefaultButtonElement = ({ onClick }: ModalButtonElementProps) => ( + <IconButton onClick={onClick}> + <LaunchIcon /> + </IconButton> +); + +export const Modal = ({ buttonElement, children, title }: ModalProps) => { + const ButtonElement = buttonElement || DefaultButtonElement; const [isOpen, setIsOpen] = React.useState<boolean>(false); return ( <> - <IconButton onClick={() => setIsOpen(true)}> - <LaunchIcon /> - </IconButton> + <ButtonElement onClick={() => setIsOpen(true)} /> <JoyModal open={isOpen} onClose={() => setIsOpen(false)}> <ModalOverflow> <ModalDialog From f3ff311d437d6c1ddc0bfc443088cf54ece34325 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:24:24 -0700 Subject: [PATCH 14/19] proper sizing of modal title --- src/components/composite/assets/config.tsx | 2 -- src/components/navigation/modal.tsx | 9 ++------- src/theme.ts | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index d15a942..17b0d66 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -29,14 +29,12 @@ export const getAssetColumns = (): GridColDef[] => [ field: 'trial_count', headerName: 'Trials', width: 85, - type: 'actions', renderCell: renderTrialModal, }, { field: 'patent_count', headerName: 'Patents', width: 85, - type: 'actions', renderCell: renderPatentModal, }, { diff --git a/src/components/navigation/modal.tsx b/src/components/navigation/modal.tsx index 1b1f2b2..87c9be8 100644 --- a/src/components/navigation/modal.tsx +++ b/src/components/navigation/modal.tsx @@ -31,14 +31,9 @@ export const Modal = ({ buttonElement, children, title }: ModalProps) => { <ButtonElement onClick={() => setIsOpen(true)} /> <JoyModal open={isOpen} onClose={() => setIsOpen(false)}> <ModalOverflow> - <ModalDialog - aria-labelledby="modal-dialog-overflow" - layout="fullscreen" - > + <ModalDialog layout="fullscreen"> <ModalClose /> - <Typography id="modal-dialog-overflow" level="h1"> - {title} - </Typography> + <Typography level="h2">{title}</Typography> {children} </ModalDialog> </ModalOverflow> diff --git a/src/theme.ts b/src/theme.ts index 19db61e..b0601de 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -21,6 +21,7 @@ const theme = extendTheme({ }, }, }, + JoyTabPanel: { styleOverrides: { root: { From 2711a021095c824d0c84a34fcab11336b6dbda36 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:25:00 -0700 Subject: [PATCH 15/19] size of modal x --- src/components/navigation/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/navigation/modal.tsx b/src/components/navigation/modal.tsx index 87c9be8..a93e9f7 100644 --- a/src/components/navigation/modal.tsx +++ b/src/components/navigation/modal.tsx @@ -32,7 +32,7 @@ export const Modal = ({ buttonElement, children, title }: ModalProps) => { <JoyModal open={isOpen} onClose={() => setIsOpen(false)}> <ModalOverflow> <ModalDialog layout="fullscreen"> - <ModalClose /> + <ModalClose size="lg" /> <Typography level="h2">{title}</Typography> {children} </ModalDialog> From a9a78a8490cc6812cc262b11a4ce1e3758178a8f Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:32:39 -0700 Subject: [PATCH 16/19] using asset to trial/patent workflow --- src/app/core/dashboard/content.tsx | 18 ------ src/app/core/dashboard/patent.tsx | 47 -------------- src/app/core/dashboard/trials.tsx | 71 ---------------------- src/components/composite/assets/client.tsx | 14 +++-- src/components/composite/assets/config.tsx | 4 +- src/components/composite/patents/index.ts | 2 + src/components/composite/trials/index.ts | 2 + 7 files changed, 13 insertions(+), 145 deletions(-) delete mode 100644 src/app/core/dashboard/patent.tsx delete mode 100644 src/app/core/dashboard/trials.tsx create mode 100644 src/components/composite/patents/index.ts create mode 100644 src/components/composite/trials/index.ts diff --git a/src/app/core/dashboard/content.tsx b/src/app/core/dashboard/content.tsx index 4613ee5..1df3107 100644 --- a/src/app/core/dashboard/content.tsx +++ b/src/app/core/dashboard/content.tsx @@ -12,11 +12,9 @@ import { Tabs } from '@/components/layout/tabs'; import { PatentSearchArgs } from '@/types/patents'; import { AssetList } from './asset'; -import { PatentList } from './patent'; // import { PatentGraph } from './graph'; import { OverTime } from './over-time'; import { Summary } from './summary'; -import { TrialList } from './trials'; export const Content = (args: PatentSearchArgs) => { try { @@ -29,22 +27,6 @@ export const Content = (args: PatentSearchArgs) => { </Suspense> ), }, - { - label: 'Patents', - panel: ( - <Suspense fallback={<Skeleton />}> - <PatentList {...args} /> - </Suspense> - ), - }, - { - label: 'Trials', - panel: ( - <Suspense fallback={<Skeleton />}> - <TrialList {...args} /> - </Suspense> - ), - }, { label: 'Summary', panel: ( diff --git a/src/app/core/dashboard/patent.tsx b/src/app/core/dashboard/patent.tsx deleted file mode 100644 index 32bce83..0000000 --- a/src/app/core/dashboard/patent.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use server'; - -import Box from '@mui/joy/Box'; -import Alert from '@mui/joy/Alert'; -import Typography from '@mui/joy/Typography'; -import WarningIcon from '@mui/icons-material/Warning'; -import 'server-only'; - -import { getPatentColumns } from '@/components/composite/patents/config'; -import { PatentDetail } from '@/components/composite/patents/client'; -import { DataGrid } from '@/components/data/grid'; -import { Patent, PatentSearchArgs } from '@/types/patents'; - -import { fetchPatents } from '../actions'; - -export const PatentList = async (args: PatentSearchArgs) => { - const columns = getPatentColumns(); - try { - const patents = await fetchPatents(args); - - return ( - <Box height="100vh"> - <DataGrid - columns={columns} - detailComponent={PatentDetail<Patent>} - rows={patents.map((patent) => ({ - ...patent, - id: patent.publication_number, - }))} - /> - </Box> - ); - } catch (e) { - return ( - <Alert - startDecorator={<WarningIcon />} - variant="soft" - color="warning" - > - <Typography level="h4">Failed to fetch patents</Typography> - <Typography> - {e instanceof Error ? e.message : JSON.stringify(e)} - </Typography> - </Alert> - ); - } -}; diff --git a/src/app/core/dashboard/trials.tsx b/src/app/core/dashboard/trials.tsx deleted file mode 100644 index 9266c70..0000000 --- a/src/app/core/dashboard/trials.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use server'; - -import Box from '@mui/joy/Box'; -import Alert from '@mui/joy/Alert'; -import Typography from '@mui/joy/Typography'; -import WarningIcon from '@mui/icons-material/Warning'; -import 'server-only'; - -import { getTrialColumns } from '@/components/composite/trials/config'; -import { TrialDetail } from '@/components/composite/trials/client'; -import { DataGrid } from '@/components/data/grid'; -import { Trial, TrialSearchArgs } from '@/types/trials'; - -import { fetchTrials } from '../actions'; - -export const TrialList = async (args: TrialSearchArgs) => { - const columns = getTrialColumns(); - try { - const trials = await fetchTrials(args); - - return ( - <Box height="100vh"> - {/* <Timeline - height={400} - pathname="" - series={[ - { - data: trials - .filter( - (t) => - t.start_date && - t.end_date && - new Date(t.end_date).getTime() > - new Date().getTime() - ) - .map((t) => ({ - x: t.mesh_conditions[0], - y: [ - new Date(t.start_date || '').getTime(), - new Date(t.end_date || '').getTime(), - ], - })) - .slice(0, 200), - }, - ]} - /> */} - <DataGrid - columns={columns} - detailComponent={TrialDetail<Trial>} - rows={trials.map((trial) => ({ - ...trial, - id: trial.nct_id, - }))} - /> - </Box> - ); - } catch (e) { - return ( - <Alert - startDecorator={<WarningIcon />} - variant="soft" - color="warning" - > - <Typography level="h4">Failed to fetch patents</Typography> - <Typography> - {e instanceof Error ? e.message : JSON.stringify(e)} - </Typography> - </Alert> - ); - } -}; diff --git a/src/components/composite/assets/client.tsx b/src/components/composite/assets/client.tsx index 1960843..472c85d 100644 --- a/src/components/composite/assets/client.tsx +++ b/src/components/composite/assets/client.tsx @@ -11,14 +11,14 @@ import { Entity } from '@/types/entities'; import { Patent } from '@/types/patents'; import { Trial } from '@/types/trials'; -import { getPatentColumns } from '../patents/config'; +import { PatentDetail, getPatentColumns } from '../patents'; import { getStyles } from '../styles'; -import { getTrialColumns } from '../trials/config'; +import { getTrialColumns, TrialDetail } from '../trials'; /** * Detail content panel for patents grid */ -export const PatentDetail = <T extends Entity>({ +export const PatentsDetail = <T extends Entity>({ row: asset, }: { row: T; @@ -29,6 +29,7 @@ export const PatentDetail = <T extends Entity>({ {asset.patents.length > 0 && ( <DataGrid columns={patentColumns} + detailComponent={PatentDetail<Patent>} getRowId={(row: Patent) => row.publication_number} rows={asset.patents} title="Patents" @@ -42,7 +43,7 @@ export const PatentDetail = <T extends Entity>({ /** * Detail content panel for patents grid */ -export const TrialDetail = <T extends Entity>({ +export const TrialsDetail = <T extends Entity>({ row: asset, }: { row: T; @@ -53,6 +54,7 @@ export const TrialDetail = <T extends Entity>({ {asset.trials.length > 0 && ( <DataGrid columns={trialColumns} + detailComponent={TrialDetail<Trial>} getRowId={(row: Trial) => row.nct_id} rows={asset.trials} title="Trials" @@ -82,7 +84,7 @@ export const TrialModal = <T extends Entity>({ row: trial, }: DocumentModalProps<T>): JSX.Element => ( <Modal buttonElement={buttonElement} title={trial.name}> - <TrialDetail row={trial} /> + <TrialsDetail row={trial} /> </Modal> ); @@ -91,7 +93,7 @@ export const PatentModal = <T extends Entity>({ buttonElement, }: DocumentModalProps<T>): JSX.Element => ( <Modal buttonElement={buttonElement} title={patent.name}> - <PatentDetail row={patent} /> + <PatentsDetail row={patent} /> </Modal> ); diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index 17b0d66..1d86c7d 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -4,7 +4,6 @@ import { GridColDef } from '@mui/x-data-grid/models/colDef'; import 'server-only'; import { - renderAssetCountChip, renderChip, renderMainTypography, renderSparkline, @@ -16,14 +15,13 @@ export const getAssetColumns = (): GridColDef[] => [ { field: 'name', headerName: 'Asset, Class or Target', - width: 300, + width: 325, renderCell: renderMainTypography, }, { field: 'approval_count', headerName: 'Approvals', width: 85, - renderCell: renderAssetCountChip, }, { field: 'trial_count', diff --git a/src/components/composite/patents/index.ts b/src/components/composite/patents/index.ts new file mode 100644 index 0000000..add3364 --- /dev/null +++ b/src/components/composite/patents/index.ts @@ -0,0 +1,2 @@ +export { PatentDetail } from './client'; +export { getPatentColumns } from './config'; diff --git a/src/components/composite/trials/index.ts b/src/components/composite/trials/index.ts new file mode 100644 index 0000000..fe339c2 --- /dev/null +++ b/src/components/composite/trials/index.ts @@ -0,0 +1,2 @@ +export { TrialDetail } from './client'; +export { getTrialColumns } from './config'; From 76f23c128860528956483ffdd1e5a8747e615ef0 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:47:08 -0700 Subject: [PATCH 17/19] owner list --- src/components/composite/assets/config.tsx | 3 ++- src/components/data/chip.tsx | 4 ++-- src/components/data/grid/formatters.tsx | 21 ++++++++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index 1d86c7d..c041a39 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -6,6 +6,7 @@ import 'server-only'; import { renderChip, renderMainTypography, + renderOwnerChip, renderSparkline, } from '@/components/data/grid'; @@ -39,7 +40,7 @@ export const getAssetColumns = (): GridColDef[] => [ field: 'owner_count', headerName: 'Owners', width: 85, - renderCell: renderChip, + renderCell: renderOwnerChip, }, { field: 'activity', diff --git a/src/components/data/chip.tsx b/src/components/data/chip.tsx index 0492c80..8ee269e 100644 --- a/src/components/data/chip.tsx +++ b/src/components/data/chip.tsx @@ -20,7 +20,7 @@ export type ChipProps = { onClick?: JoyChipProps['onClick']; size?: JoyChipProps['size']; sx?: JoyChipProps['sx']; - tooltip?: string | JSX.Element; + tooltip?: string | ReactNode; variant?: JoyChipProps['variant']; }; @@ -61,7 +61,7 @@ export const Chip = ({ if (tooltip) { return ( <Tooltip - title={<Box maxWidth={300}>{tooltip}</Box>} + title={<Box maxWidth={400}>{tooltip}</Box>} variant="outlined" > {chip} diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 43f0d11..28732a5 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -3,6 +3,8 @@ import { ReactNode } from 'react'; import TrueIcon from '@mui/icons-material/Check'; import FalseIcon from '@mui/icons-material/Close'; +import List from '@mui/joy/List'; +import ListItem from '@mui/joy/ListItem'; import Typography, { TypographyProps } from '@mui/joy/Typography'; import { GridRenderCellParams, @@ -161,7 +163,8 @@ export const renderList = ( export const getRenderChip = <T extends Record<string, unknown>>( color: ChipProps['color'], - getUrl: (row: T) => string | undefined = () => undefined + getUrl: (row: T) => string | undefined = () => undefined, + getTooltip: (row: T) => string | ReactNode | undefined = () => undefined ) => (params: GridRenderCellParams<T, string | number>): ReactNode => { const { value, row } = params; @@ -173,9 +176,10 @@ export const getRenderChip = } const href = getUrl(row); + const tooltip = getTooltip(row); return ( - <Chip color={color} href={href}> + <Chip color={color} href={href} tooltip={tooltip}> {formatLabel(value)} </Chip> ); @@ -184,9 +188,16 @@ export const getRenderChip = export const renderPrimaryChip = getRenderChip('primary'); export const renderWarningChip = getRenderChip('warning'); export const renderChip = getRenderChip('neutral'); -export const renderAssetCountChip = getRenderChip( - 'primary', - (row: { name: string }) => `/core/dashboard?terms=${row.name}` +export const renderOwnerChip = getRenderChip( + 'neutral', + undefined, + (row: { owners: string[] }) => ( + <List> + {row.owners.map((owner) => ( + <ListItem>{owner}</ListItem> + ))} + </List> + ) ); export const getRenderSparkline = From e82c8099519065a3973af6d5b55412cd9b0290bc Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:52:51 -0700 Subject: [PATCH 18/19] chip color hook --- src/components/composite/assets/config.tsx | 4 +++- src/components/data/grid/formatters.tsx | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index c041a39..e65b5bd 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -5,6 +5,7 @@ import 'server-only'; import { renderChip, + renderAvailabilityChip, renderMainTypography, renderOwnerChip, renderSparkline, @@ -56,8 +57,9 @@ export const getAssetColumns = (): GridColDef[] => [ { field: 'maybe_available_count', headerName: 'Available?', - renderCell: renderChip, + renderCell: renderAvailabilityChip, width: 100, + description: 'Number of patents that *might* be available', }, { field: 'max_phase', diff --git a/src/components/data/grid/formatters.tsx b/src/components/data/grid/formatters.tsx index 28732a5..ec04a73 100644 --- a/src/components/data/grid/formatters.tsx +++ b/src/components/data/grid/formatters.tsx @@ -162,7 +162,9 @@ export const renderList = ( */ export const getRenderChip = <T extends Record<string, unknown>>( - color: ChipProps['color'], + _color: + | ChipProps['color'] + | ((value: number) => ChipProps['color']) = 'primary', getUrl: (row: T) => string | undefined = () => undefined, getTooltip: (row: T) => string | ReactNode | undefined = () => undefined ) => @@ -178,6 +180,9 @@ export const getRenderChip = const href = getUrl(row); const tooltip = getTooltip(row); + const color = + typeof _color === 'function' ? _color(value as number) : _color; + return ( <Chip color={color} href={href} tooltip={tooltip}> {formatLabel(value)} @@ -188,6 +193,9 @@ export const getRenderChip = export const renderPrimaryChip = getRenderChip('primary'); export const renderWarningChip = getRenderChip('warning'); export const renderChip = getRenderChip('neutral'); +export const renderAvailabilityChip = getRenderChip((value) => + value > 0 ? 'success' : 'neutral' +); export const renderOwnerChip = getRenderChip( 'neutral', undefined, From d4804ebbdb383f47ea4bc3b838221a876f223057 Mon Sep 17 00:00:00 2001 From: Kristin Lindquist <lindquist.kristin@gmail.com> Date: Wed, 20 Dec 2023 18:53:22 -0700 Subject: [PATCH 19/19] approval chip --- src/components/composite/assets/config.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/composite/assets/config.tsx b/src/components/composite/assets/config.tsx index e65b5bd..24ebd8c 100644 --- a/src/components/composite/assets/config.tsx +++ b/src/components/composite/assets/config.tsx @@ -24,6 +24,7 @@ export const getAssetColumns = (): GridColDef[] => [ field: 'approval_count', headerName: 'Approvals', width: 85, + renderCell: renderChip, }, { field: 'trial_count',