-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(website): generate metadata docs from config (#3477)
* feat(website, config): Add code to generate metadata docs and description from config
- Loading branch information
1 parent
86eb930
commit dcd019d
Showing
9 changed files
with
251 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
--- | ||
import { getConfiguredOrganisms, getSchema } from '../config'; | ||
import OrganismTableSelector from './OrganismMetadataTableSelector'; | ||
import type { OrganismMetadata } from './OrganismMetadataTableSelector'; | ||
const configuredOrganisms = getConfiguredOrganisms(); | ||
const organisms: OrganismMetadata[] = configuredOrganisms.map((organism) => { | ||
return { | ||
key: organism.key, | ||
displayName: organism.displayName, | ||
metadata: getSchema(organism.key).metadata, | ||
inputFields: getSchema(organism.key).inputFields, | ||
}; | ||
}); | ||
--- | ||
|
||
<OrganismTableSelector organisms={organisms} client:load /> |
131 changes: 131 additions & 0 deletions
131
website/src/components/OrganismMetadataTableSelector.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import type { FC } from 'react'; | ||
|
||
import { routes } from '../routes/routes.ts'; | ||
import type { Metadata, InputField } from '../types/config.ts'; | ||
import { groupFieldsByHeader } from '../utils/groupFieldsByHeader.ts'; | ||
import IwwaArrowDown from '~icons/iwwa/arrow-down'; | ||
|
||
export type OrganismMetadata = { | ||
key: string; | ||
displayName: string; | ||
metadata: Metadata[]; | ||
inputFields: InputField[]; | ||
}; | ||
|
||
type Props = { | ||
organisms: OrganismMetadata[]; | ||
}; | ||
|
||
const OrganismMetadataTableSelector: FC<Props> = ({ organisms }) => { | ||
const [selectedOrganism, setSelectedOrganism] = useState<OrganismMetadata | null>(null); | ||
const [groupedFields, setGroupedFields] = useState<Map<string, InputField[]>>(new Map()); | ||
const [expandedHeaders, setExpandedHeaders] = useState<Set<string>>(new Set(['Required fields', 'Desired fields'])); | ||
|
||
const handleOrganismSelect = (event: { target: { value: string } }) => { | ||
const organismKey = event.target.value; | ||
const organism = organisms.find((o) => o.key === organismKey); | ||
setSelectedOrganism(organism ?? null); | ||
}; | ||
|
||
const toggleHeader = (header: string) => { | ||
const updatedExpandedHeaders = new Set(expandedHeaders); | ||
if (updatedExpandedHeaders.has(header)) { | ||
updatedExpandedHeaders.delete(header); // Close the table if already expanded | ||
} else { | ||
updatedExpandedHeaders.add(header); | ||
} | ||
setExpandedHeaders(updatedExpandedHeaders); | ||
}; | ||
|
||
useEffect(() => { | ||
if (selectedOrganism) { | ||
setGroupedFields(groupFieldsByHeader(selectedOrganism.inputFields, selectedOrganism.metadata)); | ||
} | ||
}, [selectedOrganism]); | ||
|
||
return ( | ||
<div> | ||
<div> | ||
<select id='organism-select' onChange={handleOrganismSelect} className='border border-gray-300 p-2'> | ||
<option value=''>-- Select an Organism --</option> | ||
{organisms.map((organism) => ( | ||
<option key={organism.key} value={organism.key}> | ||
{organism.displayName} | ||
</option> | ||
))} | ||
</select> | ||
</div> | ||
|
||
{selectedOrganism && ( | ||
<div className='mt-6'> | ||
<h1 className='text-2xl font-bold mb-4'>{selectedOrganism.displayName}</h1> | ||
<div> | ||
You can download all metadata fields and their descriptions here:{' '} | ||
<a | ||
href={routes.metadataOverview(selectedOrganism.key)} | ||
className='text-primary-700 opacity-90' | ||
> | ||
metadata_fields_descriptions.csv | ||
</a> | ||
</div> | ||
{Array.from(groupedFields.entries()).map(([header, fields]) => ( | ||
<div key={header} className='mb-8'> | ||
<h3 | ||
className='text-lg font-semibold mb-4 cursor-pointer' | ||
onClick={() => toggleHeader(header)} | ||
> | ||
{header} | ||
<IwwaArrowDown className='inline-block -mt-1 ml-1 h-4 w-4' /> | ||
</h3> | ||
<div | ||
className={`transition-all duration-300 ${ | ||
expandedHeaders.has(header) ? 'block' : 'sr-only' | ||
}`} | ||
data-table-header={header} | ||
> | ||
<MetadataTable fields={fields} metadata={selectedOrganism.metadata} /> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default OrganismMetadataTableSelector; | ||
|
||
type TableProps = { | ||
fields: InputField[]; | ||
metadata: Metadata[]; | ||
}; | ||
|
||
const MetadataTable: FC<TableProps> = ({ fields, metadata }) => { | ||
return ( | ||
<table className='table-auto border-collapse border border-gray-200 w-full'> | ||
<thead> | ||
<tr> | ||
<th className='border border-gray-300 px-4 py-2 w-[20%]'>Field Name</th> | ||
<th className='border border-gray-300 px-4 py-2 w-[13%]'>Type</th> | ||
<th className='border border-gray-300 px-4 py-2 w-[37%]'>Description</th> | ||
<th className='border border-gray-300 px-4 py-2 w-[30%]'>Example</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{fields.map((field) => { | ||
const metadataEntry = metadata.find((meta) => meta.name === field.name); | ||
|
||
return ( | ||
<tr key={field.name}> | ||
<td className='border border-gray-300 px-4 py-2'>{field.name}</td> | ||
<td className='border border-gray-300 px-4 py-2'>{metadataEntry?.type ?? 'String'}</td> | ||
<td className='border border-gray-300 px-4 py-2'>{`${field.definition ?? ''} ${field.guidance ?? ''}`}</td> | ||
<td className='border border-gray-300 px-4 py-2'>{field.example ?? ''}</td> | ||
</tr> | ||
); | ||
})} | ||
</tbody> | ||
</table> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import type { APIRoute } from 'astro'; | ||
|
||
import { cleanOrganism } from '../../../components/Navigation/cleanOrganism.ts'; | ||
import { getSchema } from '../../../config.ts'; | ||
import { SUBMISSION_ID_FIELD } from '../../../settings.ts'; | ||
|
||
export const GET: APIRoute = ({ params }) => { | ||
const rawOrganism = params.organism!; | ||
const { organism } = cleanOrganism(rawOrganism); | ||
if (organism === undefined) { | ||
return new Response(undefined, { | ||
status: 404, | ||
}); | ||
} | ||
|
||
const extraFields = [SUBMISSION_ID_FIELD]; | ||
|
||
const tableHeader = 'Field Name\tRequired\tDefinition\tGuidance\tExample'; | ||
|
||
const { inputFields } = getSchema(organism.key); | ||
|
||
const headers: Record<string, string> = { | ||
'Content-Type': 'text/tsv', // eslint-disable-line @typescript-eslint/naming-convention | ||
}; | ||
|
||
const filename = `${organism.displayName.replaceAll(' ', '_')}_metadata_overview.tsv`; | ||
headers['Content-Disposition'] = `attachment; filename="${filename}"`; | ||
|
||
const fieldNames = inputFields.map( | ||
(field) => | ||
`${field.name}\t${field.required ?? ''}\t${field.definition ?? ''} ${field.guidance ?? ''}\t${field.example ?? ''}`, | ||
); | ||
const tsvTemplate = [tableHeader, ...extraFields, ...fieldNames].join('\n'); | ||
|
||
return new Response(tsvTemplate, { | ||
headers, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import type { InputField, Metadata } from '../types/config'; | ||
|
||
const SUBMISSION_ID_FIELD: InputField = { | ||
name: 'submissionId', | ||
displayName: 'Submission ID', | ||
definition: 'FASTA ID', | ||
guidance: | ||
'Your sequence identifier; should match the FASTA file header - this is used to link the metadata to the FASTA sequence', | ||
example: 'GJP123', | ||
noEdit: true, | ||
required: true, | ||
}; | ||
|
||
export const groupFieldsByHeader = (inputFields: InputField[], metadata: Metadata[]): Map<string, InputField[]> => { | ||
const groups = new Map<string, InputField[]>(); | ||
|
||
const requiredFields = inputFields.filter((meta) => meta.required); | ||
const desiredFields = inputFields.filter((meta) => meta.desired); | ||
|
||
groups.set('Required fields', [...requiredFields, SUBMISSION_ID_FIELD]); | ||
groups.set('Desired fields', desiredFields); | ||
groups.set('Submission details', [SUBMISSION_ID_FIELD]); | ||
|
||
inputFields.forEach((field) => { | ||
const metadataEntry = metadata.find((meta) => meta.name === field.name); | ||
const header = metadataEntry?.header ?? 'Uncategorized'; | ||
|
||
if (!groups.has(header)) { | ||
groups.set(header, []); | ||
} | ||
groups.get(header)!.push({ | ||
...field, | ||
}); | ||
}); | ||
|
||
return groups; | ||
}; |