From 8fe6ad460eb93a0619f219afd9faf1182ff87e88 Mon Sep 17 00:00:00 2001 From: siddheshraze <81591724+siddheshraze@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:11:57 -0500 Subject: [PATCH] Final Pre-Demo Errors Resolved (#186) * repaired monaco instance and implementations. expanded validation system to use a table system similar to the postvalidation system, and added caching via swr and memoization to limit re-renders * forgot to add msv reloads after upload, etc. * remaining minor issues resolved --- .../measurementshub/postvalidation/page.tsx | 25 +- .../measurementshub/validations/page.tsx | 167 +- frontend/app/api/sqlload/route.ts | 14 + .../components/client/custommonacoeditor.tsx | 75 + .../components/client/postvalidationrow.tsx | 30 +- .../datagrids/applications/msvdatagrid.tsx | 34 +- .../components/processors/processcensus.tsx | 13 - .../uploadsystem/segments/uploadfireazure.tsx | 14 +- ...ncard.tsx => validationcard_cardmodal.tsx} | 61 +- .../components/validationcard_codemirror.tsx | 174 + frontend/components/validationrow.tsx | 291 + frontend/next.config.js | 14 - frontend/package-lock.json | 21301 +++++++--------- frontend/package.json | 6 + 14 files changed, 10604 insertions(+), 11615 deletions(-) create mode 100644 frontend/components/client/custommonacoeditor.tsx rename frontend/components/{validationcard.tsx => validationcard_cardmodal.tsx} (65%) create mode 100644 frontend/components/validationcard_codemirror.tsx create mode 100644 frontend/components/validationrow.tsx diff --git a/frontend/app/(hub)/measurementshub/postvalidation/page.tsx b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx index 2e28040a..66d93010 100644 --- a/frontend/app/(hub)/measurementshub/postvalidation/page.tsx +++ b/frontend/app/(hub)/measurementshub/postvalidation/page.tsx @@ -4,10 +4,10 @@ import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/conte import React, { useEffect, useState } from 'react'; import { Box, Button, Checkbox, Table, Typography, useTheme } from '@mui/joy'; import { PostValidationQueriesRDS } from '@/config/sqlrdsdefinitions/validations'; -import PostValidationRow from '@/components/client/postvalidationrow'; import { Paper, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; import { Done } from '@mui/icons-material'; import { useLoading } from '@/app/contexts/loadingprovider'; +import dynamic from 'next/dynamic'; export default function PostValidationPage() { const currentSite = useSiteContext(); @@ -17,6 +17,7 @@ export default function PostValidationPage() { const [expandedQuery, setExpandedQuery] = useState(null); const [expandedResults, setExpandedResults] = useState(null); const [selectedResults, setSelectedResults] = useState([]); + const [schemaDetails, setSchemaDetails] = useState<{ table_name: string; column_name: string }[]>([]); const replacements = { schema: currentSite?.schemaName, currentPlotID: currentPlot?.plotID, @@ -24,6 +25,8 @@ export default function PostValidationPage() { }; const { setLoading } = useLoading(); + const PostValidationRow = dynamic(() => import('@/components/client/postvalidationrow'), { ssr: false }); + const enabledPostValidations = postValidations.filter(query => query.isEnabled); const disabledPostValidations = postValidations.filter(query => !query.isEnabled); @@ -86,6 +89,24 @@ export default function PostValidationPage() { .then(() => setLoading(false)); }, []); + useEffect(() => { + const fetchSchema = async () => { + try { + const response = await fetch(`/api/structure/${currentSite?.schemaName ?? ''}`); + const data = await response.json(); + if (data.schema) { + setSchemaDetails(data.schema); + } + } catch (error) { + console.error('Error fetching schema:', error); + } + }; + + if (postValidations.length > 0) { + fetchSchema().then(r => console.log(r)); + } + }, [postValidations]); + const handleExpandClick = (queryID: number) => { setExpandedQuery(expandedQuery === queryID ? null : queryID); }; @@ -218,6 +239,7 @@ export default function PostValidationPage() { handleExpandClick={handleExpandClick} handleExpandResultsClick={handleExpandResultsClick} handleSelectResult={handleSelectResult} + schemaDetails={schemaDetails} /> ))} @@ -233,6 +255,7 @@ export default function PostValidationPage() { handleExpandClick={handleExpandClick} handleExpandResultsClick={handleExpandResultsClick} handleSelectResult={handleSelectResult} + schemaDetails={schemaDetails} /> ))} diff --git a/frontend/app/(hub)/measurementshub/validations/page.tsx b/frontend/app/(hub)/measurementshub/validations/page.tsx index 8fd2c617..cb6bd060 100644 --- a/frontend/app/(hub)/measurementshub/validations/page.tsx +++ b/frontend/app/(hub)/measurementshub/validations/page.tsx @@ -1,133 +1,98 @@ 'use client'; - -import { Box, Card, CardContent, Typography } from '@mui/joy'; -import React, { useEffect, useState } from 'react'; -import ValidationCard from '@/components/validationcard'; +import React, { useEffect, useState, useMemo } from 'react'; +import useSWR from 'swr'; import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; -import { useSiteContext } from '@/app/contexts/userselectionprovider'; +import { useOrgCensusContext, usePlotContext, useSiteContext } from '@/app/contexts/userselectionprovider'; import { useSession } from 'next-auth/react'; +import { useTheme } from '@mui/joy'; +import dynamic from 'next/dynamic'; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; + +const fetcher = (url: string) => fetch(url).then(res => res.json()); export default function ValidationsPage() { - const [globalValidations, setGlobalValidations] = React.useState([]); - const [loading, setLoading] = useState(true); // Use a loading state instead of refresh - const [schemaDetails, setSchemaDetails] = useState<{ table_name: string; column_name: string }[]>([]); const { data: session } = useSession(); - const currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const theme = useTheme(); + const isDarkMode = theme.palette.mode === 'dark'; + + const { data: globalValidations, mutate: updateValidations } = useSWR('/api/validations/crud', fetcher); + + const { data: schemaData } = useSWR<{ schema: { table_name: string; column_name: string }[] }>( + currentSite?.schemaName ? `/api/structure/${currentSite.schemaName}` : null, + fetcher + ); + + const replacements = useMemo( + () => ({ + schema: currentSite?.schemaName, + currentPlotID: currentPlot?.plotID, + currentCensusID: currentCensus?.dateRanges[0].censusID + }), + [currentSite?.schemaName, currentPlot?.plotID, currentCensus?.dateRanges] + ); + + const ValidationRow = dynamic(() => import('@/components/validationrow'), { ssr: false }); + + const [expandedValidationID, setExpandedValidationID] = useState(null); useEffect(() => { - if (session !== null && !['db admin', 'global'].includes(session.user.userStatus)) { + if (session && !['db admin', 'global'].includes(session.user.userStatus)) { throw new Error('access-denied'); } - }, []); + }, [session]); const handleSaveChanges = async (updatedValidation: ValidationProceduresRDS) => { try { - // Make the API call to toggle the validation const response = await fetch(`/api/validations/crud`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updatedValidation) // Pass the entire updated validation object + body: JSON.stringify(updatedValidation) }); if (response.ok) { - // Update the globalValidations state directly - setGlobalValidations(prev => prev.map(val => (val.validationID === updatedValidation.validationID ? updatedValidation : val))); + updateValidations(prev => (prev ? prev.map(val => (val.validationID === updatedValidation.validationID ? updatedValidation : val)) : [])); } else { - console.error('Failed to toggle validation'); + console.error('Failed to update validation'); } } catch (error) { - console.error('Error toggling validation:', error); + console.error('Error updating validation:', error); } }; - const handleDelete = async (validationID?: number) => { - try { - // Make the API call to delete the validation - const response = await fetch(`/api/validations/delete/${validationID}`, { - method: 'DELETE' - }); - if (response.ok) { - // Remove the deleted validation from the globalValidations state - setGlobalValidations(prev => prev.filter(validation => validation.validationID !== validationID)); - } else { - console.error('Failed to delete validation'); - } - } catch (error) { - console.error('Error deleting validation:', error); - } - }; - - useEffect(() => { - async function fetchValidations() { - try { - const response = await fetch('/api/validations/crud', { method: 'GET' }); - const data = await response.json(); - setGlobalValidations(data); - } catch (err) { - console.error('Error fetching validations:', err); - } finally { - setLoading(false); // Loading is complete - } - } - - fetchValidations().catch(console.error); // Initial load - }, []); - - useEffect(() => { - if (typeof window !== 'undefined') { - // Set up Monaco Editor worker path - window.MonacoEnvironment = { - getWorkerUrl: function () { - return '_next/static/[name].worker.js'; - } - }; - } - }, []); - - // Fetch schema details when component mounts - useEffect(() => { - const fetchSchema = async () => { - try { - const response = await fetch(`/api/structure/${currentSite?.schemaName ?? ''}`); - const data = await response.json(); - if (data.schema) { - setSchemaDetails(data.schema); - } - } catch (error) { - console.error('Error fetching schema:', error); - } - }; - - if (currentSite?.schemaName) { - fetchSchema().then(r => console.log(r)); - } - }, [currentSite?.schemaName]); + function handleToggleClick(incomingValidationID: number) { + setExpandedValidationID(prev => (prev === incomingValidationID ? null : incomingValidationID)); + } return ( - - - - - Review Global Validations - - {globalValidations.map(validation => ( - + + + + Enabled? + Validation + Description + Affecting Criteria + Query + Actions + + + + {globalValidations?.map((validation, index) => ( + handleToggleClick(validation.validationID!)} + isDarkMode={isDarkMode} + replacements={replacements} /> ))} - - - - - - Review Site-Specific Validations - - - - + +
+ ); } diff --git a/frontend/app/api/sqlload/route.ts b/frontend/app/api/sqlload/route.ts index c89efd52..6404bd70 100644 --- a/frontend/app/api/sqlload/route.ts +++ b/frontend/app/api/sqlload/route.ts @@ -83,6 +83,20 @@ export async function POST(request: NextRequest) { } } } + + // Update Census Start/End Dates + const combinedQuery = ` + UPDATE ${schema}.census c + JOIN ( + SELECT CensusID, MIN(MeasurementDate) AS FirstMeasurementDate, MAX(MeasurementDate) AS LastMeasurementDate + FROM ${schema}.coremeasurements + WHERE CensusID = ${censusID} + GROUP BY CensusID + ) m ON c.CensusID = m.CensusID + SET c.StartDate = m.FirstMeasurementDate, c.EndDate = m.LastMeasurementDate + WHERE c.CensusID = ${censusID};`; + + await connectionManager.executeQuery(combinedQuery); await connectionManager.closeConnection(); return new NextResponse(JSON.stringify({ message: 'Insert to SQL successful', idToRows: idToRows }), { status: HTTPResponses.OK }); } diff --git a/frontend/components/client/custommonacoeditor.tsx b/frontend/components/client/custommonacoeditor.tsx new file mode 100644 index 00000000..d06745e0 --- /dev/null +++ b/frontend/components/client/custommonacoeditor.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { useMonaco } from '@monaco-editor/react'; +import dynamic from 'next/dynamic'; +import React, { Dispatch, memo, SetStateAction, useEffect } from 'react'; + +const Editor = dynamic(() => import('@monaco-editor/react'), { ssr: false }); + +type CustomMonacoEditorProps = { + schemaDetails: { + table_name: string; + column_name: string; + }[]; + setContent?: Dispatch>; + content?: string; + height?: any; + isDarkMode?: boolean; +} & React.ComponentPropsWithoutRef; + +function CustomMonacoEditor(broadProps: CustomMonacoEditorProps) { + const { schemaDetails, setContent = () => {}, content, height, options = {}, isDarkMode, ...props } = broadProps; + const monaco = useMonaco(); + + useEffect(() => { + if (monaco) { + monaco.languages.registerCompletionItemProvider('mysql', { + provideCompletionItems: (model, position) => { + const suggestions: any[] = []; + const word = model.getWordUntilPosition(position); + const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); + + const tables = Array.from(new Set(schemaDetails.map(row => row.table_name))); + tables.forEach(table => { + suggestions.push({ + label: table, + kind: monaco.languages.CompletionItemKind.Function, + insertText: table, + detail: 'Table', + range + }); + }); + + schemaDetails.forEach(({ table_name, column_name }) => { + suggestions.push({ + label: `${table_name}.${column_name}`, + kind: monaco.languages.CompletionItemKind.Property, + insertText: `${table_name}.${column_name}`, + detail: `Column from ${table_name}`, + range + }); + }); + + return { suggestions }; + } + }); + } + }, [monaco]); + + return ( + setContent(value ?? '')} + theme={isDarkMode ? 'vs-dark' : 'light'} + options={{ + ...options, // Spread the existing options + readOnly: options.readOnly ?? false // Ensure readOnly is explicitly respected + }} + {...props} + /> + ); +} + +export default memo(CustomMonacoEditor); diff --git a/frontend/components/client/postvalidationrow.tsx b/frontend/components/client/postvalidationrow.tsx index 4cf11919..7a32c22d 100644 --- a/frontend/components/client/postvalidationrow.tsx +++ b/frontend/components/client/postvalidationrow.tsx @@ -6,13 +6,14 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { PostValidationQueriesRDS } from '@/config/sqlrdsdefinitions/validations'; import { Checkbox, IconButton, Textarea, Tooltip } from '@mui/joy'; import { Done } from '@mui/icons-material'; -import dynamic from 'next/dynamic'; import moment from 'moment/moment'; import { darken } from '@mui/system'; +import dynamic from 'next/dynamic'; interface PostValidationRowProps { postValidation: PostValidationQueriesRDS; selectedResults: PostValidationQueriesRDS[]; + schemaDetails: { table_name: string; column_name: string }[]; expanded: boolean; isDarkMode: boolean; expandedQuery: number | null; @@ -22,8 +23,6 @@ interface PostValidationRowProps { handleSelectResult: (postValidation: PostValidationQueriesRDS) => void; } -const Editor = dynamic(() => import('@monaco-editor/react'), { ssr: false }); - const PostValidationRow: React.FC = ({ expandedQuery, replacements, @@ -33,9 +32,11 @@ const PostValidationRow: React.FC = ({ handleExpandClick, handleExpandResultsClick, handleSelectResult, - selectedResults + selectedResults, + schemaDetails }) => { const formattedResults = JSON.stringify(JSON.parse(postValidation.lastRunResult ?? '{}'), null, 2); + const CustomMonacoEditor = dynamic(() => import('@/components/client/custommonacoeditor'), { ssr: false }); const successColor = !isDarkMode ? 'rgba(54, 163, 46, 0.3)' : darken('rgba(54,163,46,0.6)', 0.7); const failureColor = !isDarkMode ? 'rgba(255, 0, 0, 0.3)' : darken('rgba(255,0,0,0.6)', 0.7); @@ -119,12 +120,14 @@ const PostValidationRow: React.FC = ({ }} > {expandedQuery === postValidation.queryID ? ( - + String(replacements[p1 as keyof typeof replacements] ?? '') )} + setContent={undefined} + height={`${Math.min(300, 20 * (postValidation?.queryDefinition ?? '').split('\n').length)}px`} + isDarkMode={isDarkMode} options={{ readOnly: true, minimap: { enabled: false }, @@ -132,7 +135,6 @@ const PostValidationRow: React.FC = ({ wordWrap: 'off', lineNumbers: 'off' }} - theme={isDarkMode ? 'vs-dark' : 'light'} /> ) : (