Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display CSV file results #38

Merged
merged 7 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"rules": {
"no-console": ["error", { "allow": ["warn", "error"] }],
"no-unused-vars": [
"warn",
"error",
{ "ignoreRestSiblings": true, "varsIgnorePattern": "_+", "argsIgnorePattern": "^_" }
],
"semi": ["error", "never"]
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@tabler/icons-react": "^3.19.0",
"@tanstack/react-query": "^5.59.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/papaparse": "^5.3.15",
"@types/uuid": "^10.0.0",
"@vanilla-extract/css": "^1.16",
"@vanilla-extract/next-plugin": "^2.4",
Expand All @@ -88,6 +89,7 @@
"mantine-form-zod-resolver": "^1.1.0",
"mantine-react-table": "^2.0.0-beta.6",
"next-swagger-doc": "^0.4",
"papaparse": "^5.4.1",
"pg": "^8.13.0",
"react-icons": "^5.3.0",
"remeda": "^2.14.0",
Expand Down
17 changes: 2 additions & 15 deletions src/app/dl/results/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { b64toUUID } from '@/lib/uuid'
import { NextResponse } from 'next/server'
import { db } from '@/database'
import { urlOrPathToResultsFile } from '@/server/results'
import { MinimalRunResultsInfo } from '@/lib/types'
import { queryRunResult } from '@/server/queries'

export const GET = async (
_: Request,
Expand All @@ -15,26 +15,13 @@ export const GET = async (
const runId = b64toUUID(runIdentifier)

// TODO: check if the run is owned by the researcher
const run = await db
.selectFrom('studyRun')
.innerJoin('study', 'study.id', 'studyRun.studyId')
.innerJoin('member', 'study.memberId', 'member.id')
.select(['studyRun.id as studyRunId', 'studyId', 'resultsPath', 'member.identifier as memberIdentifier'])
.where('studyRun.id', '=', runId)
.where('studyRun.status', '=', 'COMPLETED')
.where('studyRun.resultsPath', 'is not', null)
.executeTakeFirst()
const run = await queryRunResult(runId)

if (!run) {
return NextResponse.json({ error: 'run not found', runId }, { status: 404 })
}

if (!run.resultsPath) {
return NextResponse.json({ error: 'Results not available yet' }, { status: 404 })
}

const location = await urlOrPathToResultsFile(run as MinimalRunResultsInfo)

if (location.content) {
return new NextResponse(location.content)
} else if (location.url) {
Expand Down
48 changes: 25 additions & 23 deletions src/app/researcher/study/[encodedStudyId]/review/actions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use server'

import { promises as fs } from 'fs'
import { db } from '@/database'
import { StudyStatus } from '@/database/types'
import { uuidToB64, b64toUUID } from '@/lib/uuid'
import { uuidToB64 } from '@/lib/uuid'
import { revalidatePath } from 'next/cache'
import { attachSimulatedResultsToStudyRun } from '@/server/results'
import { attachSimulatedResultsToStudyRun, storageForResultsFile } from '@/server/results'
import { sleep } from '@/lib/util'
import { SIMULATE_RESULTS_UPLOAD, USING_CONTAINER_REGISTRY } from '@/server/config'
import { queryRunResult } from '@/server/queries'
import { fetchStudyRunResults } from '@/server/aws'

const AllowedStatusChanges: Array<StudyStatus> = ['APPROVED', 'REJECTED'] as const

Expand Down Expand Up @@ -42,27 +45,6 @@ export const onFetchStudyRunsAction = async (studyId: string) => {
return runs
}

export const getLatestStudyRunAction = async ({ encodedStudyId }: { encodedStudyId: string }) => {
return await db
.selectFrom('study')
.innerJoin('member', 'member.id', 'study.memberId')
.select([
'study.id',
'study.title',
'study.containerLocation',
'member.name as memberName',
({ selectFrom }) =>
selectFrom('studyRun')
.whereRef('study.id', '=', 'studyRun.studyId')
.select('id as runId')
.orderBy('study.createdAt desc')
.limit(1)
.as('pendingRunId'),
])
.where('study.id', '=', uuidToB64(encodedStudyId))
.executeTakeFirst()
}

export const onStudyRunCreateAction = async (studyId: string) => {
const studyRun = await db
.insertInto('studyRun')
Expand Down Expand Up @@ -91,3 +73,23 @@ export const onStudyRunCreateAction = async (studyId: string) => {

return studyRun.id
}

export const fetchRunResultsAction = async (runId: string) => {
const run = await queryRunResult(runId)
if (!run) {
throw new Error(`Run ${runId} not found or does not have results`)
}
const storage = await storageForResultsFile(run)
let csv = ''
if (storage.s3) {
const body = await fetchStudyRunResults(run)
// TODO: handle other types of results that are not string/CSV
csv = await body.transformToString('utf-8')
}

if (storage.file) {
csv = await fs.readFile(storage.file, 'utf-8')
}

return csv
}
9 changes: 1 addition & 8 deletions src/app/researcher/study/[encodedStudyId]/review/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Paper, Center, Title, Stack, Group, Button } from '@mantine/core'
import { Paper, Center, Title, Stack, Group } from '@mantine/core'
import { db } from '@/database'
import { uuidToB64 } from '@/lib/uuid'
import { StudyPanel } from './panel'
Expand Down Expand Up @@ -29,13 +29,6 @@ export default async function StudyReviewPage({
<Stack>
<Group gap="xl" mb="xl">
<Title>{study.title}</Title>
{/* <Flex justify="space-between" align="center" mb="lg">
<Flex gap="md" direction="column">
<Link href={`/member/${memberIdentifier}/studies/review`}>
<Button color="blue">Back to pending review</Button>
</Link>
</Flex>
</Flex> */}
</Group>
</Stack>
<StudyPanel study={study} studyIdentifier={studyIdentifier} encodedStudyId={study.id} />
Expand Down
8 changes: 2 additions & 6 deletions src/app/researcher/study/[encodedStudyId]/review/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React from 'react'
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { Button, Group, Accordion, Stack, Text, Flex, TextInput, Textarea, Checkbox } from '@mantine/core'
import { Group, Accordion, Stack, Text, Flex, TextInput, Textarea, Checkbox } from '@mantine/core'
import { labelStyle, inputStyle } from './style.css'
import { ErrorAlert } from '@/components/errors'
import { useRouter } from 'next/navigation'
Expand All @@ -22,11 +22,7 @@ export const StudyPanel: React.FC<{ encodedStudyId: string; study: Study; studyI

const backPath = `/researcher/studies/review`

const {
mutate: updateStudy,
isPending,
error,
} = useMutation({
const { error } = useMutation({
mutationFn: (status: StudyStatus) => updateStudyStatusAction(study?.id || '', status),
onSettled(error) {
if (!error) {
Expand Down
90 changes: 90 additions & 0 deletions src/app/researcher/study/[encodedStudyId]/review/results.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client'

import { FC } from 'react'
import Link from 'next/link'
import { Button, LoadingOverlay, Modal, ScrollArea } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { useQuery } from '@tanstack/react-query'
import { uuidToB64 } from '@/lib/uuid'
import Papa from 'papaparse'
import { DataTable } from 'mantine-datatable'
import { fetchRunResultsAction } from './actions'
import { ErrorAlert } from '@/components/errors'
import { IconDownload } from '@tabler/icons-react'

type RunResultsProps = {
run: { id: string }
}

const ViewCSV: FC<RunResultsProps> = ({ run }) => {
const {
data: csv,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['run-results', run.id],
queryFn: async () => {
const csv = await fetchRunResultsAction(run.id)
return Papa.parse<Record<string, string | number>>(csv, {
header: true,
complete: (results) => {
results.data.forEach((row, i) => {
row['INDEX_KEY_FOR_RENDERING'] = i
})
return results
},
})
},
})

if (isError) {
return <ErrorAlert error={error} />
}

if (isLoading || !csv) {
return <LoadingOverlay />
}

return (
<DataTable
height={'calc(100vh - 200px)'}
idAccessor={'INDEX_KEY_FOR_RENDERING'}
withTableBorder={false}
withColumnBorders={false}
records={csv.data}
columns={(csv?.meta?.fields || []).map((f) => ({ accessor: f }))}
/>
)
}

export const PreviewCSVResultsBtn: FC<RunResultsProps> = ({ run }) => {
const [opened, { open, close }] = useDisclosure(false)

return (
<>
<Modal
opened={opened}
onClose={close}
size="100%"
title={
<Link href={`/dl/results/${uuidToB64(run.id)}`}>
<Button rightSection={<IconDownload size={14} />}>Download Results</Button>
</Link>
}
scrollAreaComponent={ScrollArea.Autosize}
overlayProps={{
backgroundOpacity: 0.55,
blur: 3,
}}
centered
>
<ViewCSV run={run} />
</Modal>

<Button variant="outline" onClick={open}>
View Results
</Button>
</>
)
}
Loading