Skip to content

Commit

Permalink
Minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
illusional committed Jun 12, 2024
1 parent 426f6f2 commit 8eb028e
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 63 deletions.
18 changes: 9 additions & 9 deletions api/routes/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

from api.utils.db import (
Connection,
Expand All @@ -26,6 +25,7 @@
from db.python.tables.participant import ParticipantFilter
from db.python.tables.project import ProjectPermissionsTable
from db.python.utils import GenericFilter, GenericMetaFilter
from models.base import SMBase
from models.enums.web import SeqrDatasetType
from models.models.participant import NestedParticipant
from models.models.project import ProjectId
Expand All @@ -35,7 +35,7 @@
from models.utils.sequencing_group_id_format import sequencing_group_id_transform_to_raw


class SearchResponseModel(BaseModel):
class SearchResponseModel(SMBase):
"""Parent model class, allows flexibility later on"""

responses: list[SearchResponse]
Expand All @@ -60,10 +60,10 @@ async def get_project_summary(
return summary.to_external()


class ProjectParticipantGridFilter(BaseModel):
class ProjectParticipantGridFilter(SMBase):
"""filters for participant grid"""

class ParticipantGridParticipantFilter(BaseModel):
class ParticipantGridParticipantFilter(SMBase):
"""participant filter option for participant grid"""

id: GenericFilter[int] | None = None
Expand All @@ -73,22 +73,22 @@ class ParticipantGridParticipantFilter(BaseModel):
reported_gender: GenericFilter[str] | None = None
karyotype: GenericFilter[str] | None = None

class ParticipantGridFamilyFilter(BaseModel):
class ParticipantGridFamilyFilter(SMBase):
"""family filter option for participant grid"""

id: GenericFilter[int] | None = None
external_id: GenericFilter[str] | None = None
meta: GenericMetaFilter | None = None

class ParticipantGridSampleFilter(BaseModel):
class ParticipantGridSampleFilter(SMBase):
"""sample filter option for participant grid"""

id: GenericFilter[str] | None = None
type: GenericFilter[str] | None = None
external_id: GenericFilter[str] | None = None
meta: GenericMetaFilter | None = None

class ParticipantGridSequencingGroupFilter(BaseModel):
class ParticipantGridSequencingGroupFilter(SMBase):
"""sequencing group filter option for participant grid"""

id: GenericFilter[str] | None = None
Expand All @@ -98,7 +98,7 @@ class ParticipantGridSequencingGroupFilter(BaseModel):
technology: GenericFilter[str] | None = None
platform: GenericFilter[str] | None = None

class ParticipantGridAssayFilter(BaseModel):
class ParticipantGridAssayFilter(SMBase):
"""assay filter option for participant grid"""

id: GenericFilter[int] | None = None
Expand Down Expand Up @@ -211,7 +211,7 @@ async def get_project_summary_with_limit(
)


class ExportProjectParticipantFields(BaseModel):
class ExportProjectParticipantFields(SMBase):
"""fields for exporting project participants"""

family_keys: list[str]
Expand Down
21 changes: 21 additions & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
--color-bg-card: #fff;
--color-bg-disabled-card: #eeeeee;
--color-table-header: #eaeaea;
--color-text-red: #ff0000;

--color-border-color: #c8c9ca;
--color-border-default: #d0d7de;
--color-border-red: #f04848;

--color-divider: rgba(0, 0, 0, 0.3);

Expand Down Expand Up @@ -43,11 +45,13 @@ html[data-theme='dark-mode'] {

--color-border-color: #3a3a3a;
--color-border-default: #292a2b;
--color-border-red: #921111;
--color-divider: rgba(255, 255, 255, 0.2);

--color-text-primary: rgba(255, 255, 255, 0.87);
--color-text-medium: rgba(255, 255, 255, 0.6);
--color-text-disabled: rgba(255, 255, 255, 0.38);
--color-text-red: #fb6f6f;

--color-check-green: #659251;
--color-table-header: rgba(0, 0, 0, 0.15);
Expand Down Expand Up @@ -150,6 +154,23 @@ blockquote {
width: 100% !important;
}

.ui.accordion .title:not(.ui) {
color: var(--color-text-primary);
}

.ui .card {
background: var(--color-bg-card);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
}

textarea {
background: var(--color-bg);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);

}

/* https://alexandergottlieb.com/2018/02/22/overflow-scroll-and-the-right-padding-problem-a-css-only-solution/ */
.projectSummaryGrid::after {
content: '';
Expand Down
70 changes: 62 additions & 8 deletions web/src/pages/project/ExportProjectButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { Button, Message } from 'semantic-ui-react'
import { Button, ButtonGroup, Dropdown, Message } from 'semantic-ui-react'

import _ from 'lodash'
import {
Expand All @@ -10,18 +10,33 @@ import {
} from '../../sm-api/api'
import { MetaSearchEntityPrefix, ProjectGridHeaderGroup } from './ProjectColumnOptions'

const extensionFromExportType = (exportType: ExportType) => {
switch (exportType) {
case ExportType.Csv:
return 'csv'
case ExportType.Tsv:
return 'tsv'
case ExportType.Json:
return 'json'
default:
return 'txt'
}
}

export const ProjectExportButton: React.FunctionComponent<{
participants_in_query: number
projectName: string

filterValues: ProjectParticipantGridFilter
headerGroups: ProjectGridHeaderGroup[]
}> = ({ filterValues, projectName, headerGroups, participants_in_query }) => {
const [exportType, setExportType] = React.useState<ExportType>(ExportType.Csv)
const [isDownloading, setIsDownloading] = React.useState(false)
const [downloadError, setDownloadError] = React.useState<string | undefined>(undefined)
// const keys = Object.keys(headerGroups).reduce((prev, k) => ({...prev, k}), {})

const download = () => {
const download = (_exportType: ExportType) => {
if (!_exportType) return
setIsDownloading(true)
setDownloadError(undefined)

Expand All @@ -45,16 +60,21 @@ export const ProjectExportButton: React.FunctionComponent<{
.map((f) => f.name),
}
new WebApi()
.exportProjectParticipants(ExportType.Csv, projectName, {
.exportProjectParticipants(_exportType, projectName, {
query: filterValues,
fields: fields,
})
.then((resp) => {
setIsDownloading(false)
const url = window.URL.createObjectURL(new Blob([resp.data]))
let data = resp.data
if (_exportType == ExportType.Json) {
data = JSON.stringify(data)
}
const url = window.URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
link.href = url
const defaultFilename = `project-export-${projectName}.csv`
const ext = extensionFromExportType(_exportType)
const defaultFilename = `project-export-${projectName}.${ext}`
link.setAttribute(
'download',
resp.headers['content-disposition']?.split('=')?.[1] || defaultFilename
Expand All @@ -64,11 +84,45 @@ export const ProjectExportButton: React.FunctionComponent<{
})
.catch((er) => setDownloadError(er.message))
}

// onClick={() => download(ExportType.Csv)
const exportOptions = [
{
key: 'csv',
text: 'CSV',
value: ExportType.Csv,
},
{
key: 'tsv',
text: 'TSV',
value: ExportType.Tsv,
},
{
key: 'json',
text: 'JSON (all columns)',
value: ExportType.Json,
},
]

return (
<>
<Button loading={isDownloading} onClick={download}>
Export {participants_in_query} participants: CSV
</Button>
<ButtonGroup>
<Button loading={isDownloading} onClick={() => download(exportType)}>
Export {participants_in_query} participants
</Button>
<Dropdown
className="button icon"
icon="caret down"
floating
value={exportType}
onChange={(e, data) => {
debugger
setExportType(data.value as ExportType)
download(data.value as ExportType)
}}
options={exportOptions}
/>
</ButtonGroup>
{downloadError && (
<Message error>
<br />
Expand Down
76 changes: 76 additions & 0 deletions web/src/pages/project/JsonEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react'
import { Button } from 'semantic-ui-react'

interface JsonEditorProps {
jsonStr?: string
jsonObj?: any
onChange: (json: object) => void
}

export const JsonEditor: React.FunctionComponent<JsonEditorProps> = ({
jsonStr,
jsonObj,
onChange,
}) => {
const [innerJsonValue, setInnerJsonValue] = React.useState<string>(
jsonObj ? JSON.stringify(jsonObj, null, 2) : jsonStr || ''
)
const [error, setError] = React.useState<string | undefined>(undefined)

React.useEffect(() => {
if (jsonObj) {
setInnerJsonValue(JSON.stringify(jsonObj, null, 2))
return
}
if (jsonStr) {
setInnerJsonValue(jsonStr)
}
}, [jsonStr, jsonObj])

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value
setInnerJsonValue(value)
try {
const newJson = JSON.parse(value)
setError(undefined)
} catch (e: any) {
setError(e.message)
}
}

const submit = () => {
try {
const newJson = JSON.parse(innerJsonValue)
onChange(newJson)
} catch (e: any) {
setError(e.message)
}
}

return (
<div>
<textarea
value={innerJsonValue}
onChange={handleChange}
style={{
width: '100%',
height: '400px',
border: !!error ? '3px solid var(--color-border-red)' : '1px solid #ccc',
padding: '10px',
fontFamily:
'Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace',
}}
/>
{error && (
<p>
<em style={{ color: 'var(--color-text-red)' }}>{error}</em>
</p>
)}
<p>
<Button onClick={submit} disabled={!!error}>
Apply
</Button>
</p>
</div>
)
}
23 changes: 17 additions & 6 deletions web/src/pages/project/ProjectColumnOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Segment,
} from 'semantic-ui-react'
import { ProjectParticipantGridFilter, ProjectParticipantGridResponse } from '../../sm-api'
import { ValueFilter } from './ValueFilter'
import { JsonEditor } from './JsonEditor'

export enum MetaSearchEntityPrefix {
F = 'family',
Expand Down Expand Up @@ -130,7 +130,13 @@ export const ProjectColumnOptions: React.FC<ProjectColumnOptionsProps> = ({
}

return (
<Card style={{ width: screen.width, padding: '20px' }}>
<Card
style={{
width: screen.width,
padding: '20px',
backgroundColor: 'var(--color-bg-card)',
}}
>
<Accordion>
<AccordionTitle active={isOpen} onClick={() => setIsOpen(!isOpen)}>
<h3>Filter + display options</h3>
Expand All @@ -143,10 +149,15 @@ export const ProjectColumnOptions: React.FC<ProjectColumnOptionsProps> = ({
appear to freeze
</Message>
)}
<Grid container columns={3} divided>
<JsonEditor jsonObj={filterValues} onChange={updateFilters} />
<br />
<Grid container divided>
{headerGroups.map(({ category, fields }) => {
return (
<Segment key={`project-col-option-${category}`}>
<Segment
key={`project-col-option-${category}`}
style={{ marginLeft: '10px' }}
>
<h3>{_.startCase(category.replaceAll('_', ' '))}</h3>
<table
style={{
Expand All @@ -173,15 +184,15 @@ export const ProjectColumnOptions: React.FC<ProjectColumnOptionsProps> = ({
}
/>
</td>
<td style={{ paddingLeft: '10px' }}>
{/* <td style={{ paddingLeft: '10px' }}>
<ValueFilter
category={category}
filterKey={field.name}
filterValues={filterValues}
updateFilterValues={updateFilters}
size="small"
/>
</td>
</td> */}
</tr>
)
})}
Expand Down
Loading

0 comments on commit 8eb028e

Please sign in to comment.