diff --git a/src/app/researcher/studies/page.tsx b/src/app/researcher/studies/page.tsx index 2e21ad2..5e48822 100644 --- a/src/app/researcher/studies/page.tsx +++ b/src/app/researcher/studies/page.tsx @@ -1,7 +1,7 @@ import { db } from '@/database' import { Container, Flex, Button, Paper, Title, Group, Alert, Anchor } from '@mantine/core' import Link from '../../../../node_modules/next/link' -import { b64toUUID } from '@/lib/uuid' +import { uuidToB64 } from '@/lib/uuid' import { studyRowStyle, studyStatusStyle, studyTitleStyle } from './styles.css' import { humanizeStatus } from '@/lib/status' @@ -41,9 +41,9 @@ export default async function StudyReviewPage() {

{study.title}

{study.piName}

{humanizeStatus(study.status)}

- - Proceed to review ≫ - + + Proceed to review ≫ + ))} diff --git a/src/app/researcher/study/[encodedStudyId]/edit/schema.ts b/src/app/researcher/study/[encodedStudyId]/edit/schema.ts index 22d6c5d..96f16d7 100644 --- a/src/app/researcher/study/[encodedStudyId]/edit/schema.ts +++ b/src/app/researcher/study/[encodedStudyId]/edit/schema.ts @@ -5,8 +5,8 @@ import { zodResolver } from 'mantine-form-zod-resolver' const schema = z .object({ title: z.string().min(3).max(100), - description: z.string().min(3).max(100), - piName: z.string().min(3).max(100), + description: z.string().min(3).max(500), + piName: z.string().min(3).max(1500), highlights: z.boolean().nullish(), // .preprocess((value) => value === 'on', z.boolean()).nullish(), eventCapture: z.boolean().nullish(), outputMimeType: z.string().nullish(), diff --git a/src/app/researcher/study/[encodedStudyId]/review/page.tsx b/src/app/researcher/study/[encodedStudyId]/review/page.tsx index 20f13b9..d001431 100644 --- a/src/app/researcher/study/[encodedStudyId]/review/page.tsx +++ b/src/app/researcher/study/[encodedStudyId]/review/page.tsx @@ -1,6 +1,6 @@ import { Paper, Center, Title, Stack, Group } from '@mantine/core' import { db } from '@/database' -import { uuidToB64 } from '@/lib/uuid' +import { b64toUUID } from '@/lib/uuid' import { StudyPanel } from './panel' import { AlertNotFound } from '@/components/errors' import { ResearcherBreadcrumbs } from '@/components/page-breadcrumbs' @@ -15,7 +15,7 @@ export default async function StudyReviewPage({ const study = await db .selectFrom('study') .selectAll() - .where('id', '=', uuidToB64(encodedStudyId)) + .where('id', '=', b64toUUID(encodedStudyId)) .executeTakeFirst() if (!study) { diff --git a/src/app/researcher/study/[encodedStudyId]/review/results.tsx b/src/app/researcher/study/[encodedStudyId]/review/results.tsx index 66b8e3a..1582916 100644 --- a/src/app/researcher/study/[encodedStudyId]/review/results.tsx +++ b/src/app/researcher/study/[encodedStudyId]/review/results.tsx @@ -11,9 +11,11 @@ import { DataTable } from 'mantine-datatable' import { fetchRunResultsAction } from './actions' import { ErrorAlert } from '@/components/errors' import { IconDownload } from '@tabler/icons-react' +import { slugify } from '@/lib/string' type RunResultsProps = { run: { id: string } + study: { title: string } } const ViewCSV: FC = ({ run }) => { @@ -48,7 +50,7 @@ const ViewCSV: FC = ({ run }) => { return ( = ({ run }) => { ) } -export const PreviewCSVResultsBtn: FC = ({ run }) => { +export const PreviewCSVResultsBtn: FC = ({ run, study }) => { const [opened, { open, close }] = useDisclosure(false) return ( @@ -68,7 +70,7 @@ export const PreviewCSVResultsBtn: FC = ({ run }) => { onClose={close} size="100%" title={ - + } @@ -79,7 +81,7 @@ export const PreviewCSVResultsBtn: FC = ({ run }) => { }} centered > - + - + setOpened(false)} + title="AWS Instructions" + centered + > + + )} {run.status == 'COMPLETED' && } diff --git a/src/app/researcher/study/request/[memberIdentifier]/actions.ts b/src/app/researcher/study/request/[memberIdentifier]/actions.ts index e454cb5..116a76c 100644 --- a/src/app/researcher/study/request/[memberIdentifier]/actions.ts +++ b/src/app/researcher/study/request/[memberIdentifier]/actions.ts @@ -7,6 +7,7 @@ import { db } from '@/database' import { uuidToB64 } from '@/lib/uuid' import { v7 as uuidv7 } from 'uuid' import { onStudyRunCreateAction } from '@/app/researcher/studies/actions' +import { strToAscii } from '@/lib/string' export const onCreateStudyAction = async (memberId: string, study: FormValues) => { schema.parse(study) // throws when malformed @@ -25,7 +26,7 @@ export const onCreateStudyAction = async (memberId: string, study: FormValues) = if (USING_CONTAINER_REGISTRY) { repoUrl = await createAnalysisRepository(repoPath, { - title: study.title, + title: strToAscii(study.title), studyId, }) } else { diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 79b6045..28e968d 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -1,24 +1,5 @@ import type { MinimalRunInfo, MinimalRunResultsInfo } from '@/lib/types' import { uuidToB64 } from './uuid' -// // https://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/ -export function slugify(str: string) { - str = str.replace(/^\s+|\s+$/g, '') // trim - str = str.toLowerCase() - - // remove accents, swap ñ for n, etc - var from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;' - var to = 'aaaaeeeeiiiioooouuuunc------' - for (var i = 0, l = from.length; i < l; i++) { - str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) - } - - str = str - .replace(/[^a-z0-9 -]/g, '') // remove invalid chars - .replace(/\s+/g, '-') // collapse whitespace and replace by - - .replace(/-+/g, '-') // collapse dashes - - return str.slice(0, 50) -} export const pathForStudyRun = (parts: MinimalRunInfo) => `analysis/${parts.memberIdentifier}/${parts.studyId}/${parts.studyRunId}` diff --git a/src/lib/string.ts b/src/lib/string.ts new file mode 100644 index 0000000..cac55f7 --- /dev/null +++ b/src/lib/string.ts @@ -0,0 +1,23 @@ +export function strToAscii(str: string) { + return str.replace(/[^\x00-\x7F]/g, '') +} + +// https://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/ +export function slugify(str: string) { + str = str.replace(/^\s+|\s+$/g, '') // trim + str = str.toLowerCase() + + // remove accents, swap ñ for n, etc + var from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;' + var to = 'aaaaeeeeiiiioooouuuunc------' + for (var i = 0, l = from.length; i < l; i++) { + str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) + } + + str = str + .replace(/[^a-z0-9 -]/g, '') // remove invalid chars + .replace(/\s+/g, '-') // collapse whitespace and replace by - + .replace(/-+/g, '-') // collapse dashes + + return str.slice(0, 50) +} diff --git a/src/server/aws.ts b/src/server/aws.ts index f863ad9..d706ab2 100644 --- a/src/server/aws.ts +++ b/src/server/aws.ts @@ -3,18 +3,22 @@ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3' import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import { Upload } from '@aws-sdk/lib-storage' import { ECRClient, CreateRepositoryCommand, SetRepositoryPolicyCommand } from '@aws-sdk/client-ecr' -import { TEST_ENV } from './config' +import { AWS_ACCOUNT_ENVIRONMENT, TEST_ENV } from './config' import { fromIni } from '@aws-sdk/credential-providers' -import { slugify, pathForStudyRun, pathForStudyRunResults } from '@/lib/paths' +import { strToAscii, slugify } from '@/lib/string' +import { pathForStudyRun, pathForStudyRunResults } from '@/lib/paths' import { uuidToB64 } from '@/lib/uuid' import { Readable } from 'stream' import { createHash } from 'crypto' import { CodeManifest, MinimalRunInfo, MinimalRunResultsInfo } from '@/lib/types' import { getECRPolicy } from './aws-ecr-policy' -const DEFAULT_TAGS: Record = { - Environment: 'sandbox', - Target: 'si:analysis', +export function objectToAWSTags(tags: Record) { + const Environment = AWS_ACCOUNT_ENVIRONMENT[process.env.AWS_ACCOUNT_ID || ''] || 'Unknown' + return Object.entries({ ...tags, Environment, Application: 'Mangement App' }).map(([Key, Value]) => ({ + Key, + Value: strToAscii(Value).slice(0, 256), + })) } let _ecrClient: ECRClient | null = null @@ -70,7 +74,7 @@ export async function createAnalysisRepository(repositoryName: string, tags: Rec const resp = await ecrClient.send( new CreateRepositoryCommand({ repositoryName, - tags: Object.entries(Object.assign(tags, DEFAULT_TAGS)).map(([Key, Value]) => ({ Key, Value })), + tags: objectToAWSTags({ ...tags, Target: 'si:analysis' }), }), ) if (!resp?.repository?.repositoryUri) { @@ -105,10 +109,7 @@ export const storeResultsFile = async (info: MinimalRunResultsInfo, body: Readab const hash = await calculateChecksum(csStream) const uploader = new Upload({ client: getS3Client(), - tags: [ - { Key: 'studyId', Value: info.studyId }, - { Key: 'runId', Value: info.studyRunId }, - ], + tags: objectToAWSTags({ studyId: info.studyId, runId: info.studyRunId }), params: { Bucket: s3BucketName(), ChecksumSHA256: hash, diff --git a/src/server/config.ts b/src/server/config.ts index e446dfc..88b9259 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -21,3 +21,10 @@ export const ENCLAVE_AWS_ACCOUNT_NUMBERS = [ '084375557107', // dev '354918363956', // sandbox ] + +export const AWS_ACCOUNT_ENVIRONMENT: Record = { + '533267019973': 'Production', + '867344442985': 'Staging', + '905418271997': 'Sandbox', + '872515273917': 'Development', +}