diff --git a/frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.ts b/frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.ts index 2c27397e..efd5c1e5 100644 --- a/frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.ts +++ b/frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.ts @@ -4,10 +4,7 @@ import { FailedMeasurementsRDS } from '@/config/sqlrdsdefinitions/core'; import connectionmanager from '@/config/connectionmanager'; import { format } from 'mysql2/promise'; -export async function POST( - request: NextRequest, - props: { params: Promise<{ schema: string; slugs?: string[] }> } -) { +export async function POST(request: NextRequest, props: { params: Promise<{ schema: string; slugs?: string[] }> }) { const params = await props.params; let errorRows: FailedMeasurementsRDS[] = await request.json(); const schema = params.schema; diff --git a/frontend/app/api/bulkcrud/route.ts b/frontend/app/api/bulkcrud/route.ts index 573f14ad..63d6280c 100644 --- a/frontend/app/api/bulkcrud/route.ts +++ b/frontend/app/api/bulkcrud/route.ts @@ -7,7 +7,7 @@ import ConnectionManager from '@/config/connectionmanager'; import { Plot } from '@/config/sqlrdsdefinitions/zones'; import { OrgCensus } from '@/config/sqlrdsdefinitions/timekeeping'; -export async function POST(request: NextRequest, { params }: { params: { dataType: string; slugs?: string[] } }) { +export async function POST(request: NextRequest) { const body = await request.json(); const dataType: string = body.gridType; const schema: string = body.schema; diff --git a/frontend/app/api/catalog/[firstName]/[lastName]/route.ts b/frontend/app/api/catalog/[firstName]/[lastName]/route.ts index 017ad301..c1c362af 100644 --- a/frontend/app/api/catalog/[firstName]/[lastName]/route.ts +++ b/frontend/app/api/catalog/[firstName]/[lastName]/route.ts @@ -2,10 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { HTTPResponses } from '@/config/macros'; import ConnectionManager from '@/config/connectionmanager'; -export async function GET( - _request: NextRequest, - props: { params: Promise<{ firstName: string; lastName: string }> } -) { +export async function GET(_request: NextRequest, props: { params: Promise<{ firstName: string; lastName: string }> }) { const params = await props.params; const { firstName, lastName } = params; if (!firstName || !lastName) throw new Error('no first or last name provided!'); diff --git a/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts b/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts index d74965c2..a327d990 100644 --- a/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts +++ b/frontend/app/api/changelog/overview/[changelogType]/[[...options]]/route.ts @@ -3,10 +3,7 @@ import { HTTPResponses } from '@/config/macros'; import MapperFactory from '@/config/datamapper'; import ConnectionManager from '@/config/connectionmanager'; -export async function GET( - request: NextRequest, - props: { params: Promise<{ changelogType: string; options?: string[] }> } -) { +export async function GET(request: NextRequest, props: { params: Promise<{ changelogType: string; options?: string[] }> }) { const params = await props.params; const schema = request.nextUrl.searchParams.get('schema'); if (!schema) throw new Error('schema not found'); diff --git a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts index a610ce41..641dd8d4 100644 --- a/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/cmprevalidation/[dataType]/[[...slugs]]/route.ts @@ -4,10 +4,7 @@ import ConnectionManager from '@/config/connectionmanager'; // datatype: table name // expecting 1) schema 2) plotID 3) plotCensusNumber -export async function GET( - _request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function GET(_request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs || !params.dataType) throw new Error('missing slugs'); const [schema, plotID, plotCensusNumber] = params.slugs; diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts index ded828bf..490ba39b 100644 --- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts @@ -136,10 +136,7 @@ export async function GET( } // required dynamic parameters: dataType (fixed),[ schema, gridID value] -> slugs -export async function POST( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function POST(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, gridID, _plotIDParam, censusIDParam] = params.slugs; @@ -204,10 +201,7 @@ export async function POST( } // slugs: schema, gridID -export async function PATCH( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function PATCH(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, gridID] = params.slugs; @@ -345,10 +339,7 @@ export async function PATCH( // slugs: schema, gridID // body: full data row, only need first item from it this time though -export async function DELETE( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function DELETE(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, gridID] = params.slugs; diff --git a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts index b92ffb7f..a5a94c71 100644 --- a/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddatafilter/[dataType]/[[...slugs]]/route.ts @@ -373,10 +373,7 @@ export async function POST( } // slugs: schema, gridID -export async function PATCH( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function PATCH(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, gridID] = params.slugs; @@ -437,10 +434,7 @@ export async function PATCH( // slugs: schema, gridID // body: full data row, only need first item from it this time though -export async function DELETE( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function DELETE(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, gridID] = params.slugs; diff --git a/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts index e0abfaa0..61552f3b 100644 --- a/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/formdownload/[dataType]/[[...slugs]]/route.ts @@ -34,10 +34,7 @@ const buildSearchStub = (columns: string[], quickFilter: string[], alias?: strin .join(' OR '); }; -export async function GET( - _request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function GET(_request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; const { dataType, slugs } = params; if (!dataType || !slugs) throw new Error('data type or slugs not provided'); diff --git a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts index 154997ac..c8b56399 100644 --- a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts @@ -7,10 +7,7 @@ import ConnectionManager from '@/config/connectionmanager'; // slugs: schema, columnName, value ONLY // needs to match dynamic format established by other slug routes! // refit to match entire rows, using dataType convention to determine what columns need testing? -export async function GET( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function GET(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; // simple dynamic validation to confirm table input values: if (!params.slugs || params.slugs.length !== 3) throw new Error('slugs missing -- formvalidation'); diff --git a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts index 14c2362c..cc42ee50 100644 --- a/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts +++ b/frontend/app/api/postvalidationbyquery/[schema]/[plotID]/[censusID]/[queryID]/route.ts @@ -3,10 +3,7 @@ import { HTTPResponses } from '@/config/macros'; import moment from 'moment'; import ConnectionManager from '@/config/connectionmanager'; -export async function GET( - _request: NextRequest, - props: { params: Promise<{ schema: string; plotID: string; censusID: string; queryID: string }> } -) { +export async function GET(_request: NextRequest, props: { params: Promise<{ schema: string; plotID: string; censusID: string; queryID: string }> }) { const params = await props.params; const { schema } = params; const plotID = parseInt(params.plotID); diff --git a/frontend/app/api/refreshviews/[view]/[schema]/route.ts b/frontend/app/api/refreshviews/[view]/[schema]/route.ts index c308be00..6c0ce992 100644 --- a/frontend/app/api/refreshviews/[view]/[schema]/route.ts +++ b/frontend/app/api/refreshviews/[view]/[schema]/route.ts @@ -2,10 +2,7 @@ import { HTTPResponses } from '@/config/macros'; import { NextRequest, NextResponse } from 'next/server'; import ConnectionManager from '@/config/connectionmanager'; -export async function POST( - _request: NextRequest, - props: { params: Promise<{ view: string; schema: string }> } -) { +export async function POST(_request: NextRequest, props: { params: Promise<{ view: string; schema: string }> }) { const params = await props.params; if (!params.schema || params.schema === 'undefined' || !params.view || params.view === 'undefined' || !params) throw new Error('schema not provided'); const { view, schema } = params; diff --git a/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts index e5c7ef36..60eda8a8 100644 --- a/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/rollover/[dataType]/[[...slugs]]/route.ts @@ -9,10 +9,7 @@ import ConnectionManager from '@/config/connectionmanager'; * @param params - The route parameters, including the `dataType` (either 'quadrats' or 'personnel') and the `slugs` (an array containing the schema, plotID, sourceCensusID, and newCensusID). * @returns A NextResponse with a success message or an error message, along with the appropriate HTTP status code. */ -export async function POST( - request: NextRequest, - props: { params: Promise<{ dataType: string; slugs?: string[] }> } -) { +export async function POST(request: NextRequest, props: { params: Promise<{ dataType: string; slugs?: string[] }> }) { const params = await props.params; if (!params.slugs) throw new Error('slugs not provided'); const [schema, plotID, sourceCensusID, newCensusID] = params.slugs; diff --git a/frontend/app/api/specieslimits/[plotID]/[plotCensusNumber]/route.ts b/frontend/app/api/specieslimits/[plotID]/[plotCensusNumber]/route.ts index 385a15e2..da807868 100644 --- a/frontend/app/api/specieslimits/[plotID]/[plotCensusNumber]/route.ts +++ b/frontend/app/api/specieslimits/[plotID]/[plotCensusNumber]/route.ts @@ -4,10 +4,7 @@ import { HTTPResponses } from '@/config/macros'; import ConnectionManager from '@/config/connectionmanager'; // pulls everything -export async function GET( - request: NextRequest, - props: { params: Promise<{ plotID: string; plotCensusNumber: string }> } -) { +export async function GET(request: NextRequest, props: { params: Promise<{ plotID: string; plotCensusNumber: string }> }) { const params = await props.params; const schema = request.nextUrl.searchParams.get('schema'); if (!schema) throw new Error('Schema not provided'); diff --git a/frontend/app/api/validations/procedures/[validationType]/route.ts b/frontend/app/api/validations/procedures/[validationType]/route.ts index f7416ba9..a253e1aa 100644 --- a/frontend/app/api/validations/procedures/[validationType]/route.ts +++ b/frontend/app/api/validations/procedures/[validationType]/route.ts @@ -2,10 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { runValidation } from '@/components/processors/processorhelperfunctions'; import { HTTPResponses } from '@/config/macros'; -export async function POST( - request: NextRequest, - props: { params: Promise<{ validationType: string }> } -) { +export async function POST(request: NextRequest, props: { params: Promise<{ validationType: string }> }) { const params = await props.params; try { if (!params.validationType) throw new Error('validationProcedureName not provided'); diff --git a/frontend/app/contexts/listselectionprovider.tsx b/frontend/app/contexts/listselectionprovider.tsx index ac7870a2..753e20fc 100644 --- a/frontend/app/contexts/listselectionprovider.tsx +++ b/frontend/app/contexts/listselectionprovider.tsx @@ -1,6 +1,6 @@ // ListSelectionProvider.tsx 'use client'; -import React, { createContext, Dispatch, useContext, useReducer } from 'react'; +import React, { createContext, Dispatch, Reducer, useContext, useReducer } from 'react'; import { createEnhancedDispatch, EnhancedDispatch, genericLoadReducer, LoadAction } from '@/config/macros/contextreducers'; import { PlotRDS, QuadratRDS, SitesRDS } from '@/config/sqlrdsdefinitions/zones'; import { OrgCensus } from '@/config/sqlrdsdefinitions/timekeeping'; @@ -19,13 +19,18 @@ export const SiteListDispatchContext = createContext | undefined>(undefined); export function ListSelectionProvider({ children }: Readonly<{ children: React.ReactNode }>) { - const [plotList, plotListDispatch] = useReducer>>(genericLoadReducer, []); + const plotListReducer: Reducer> = genericLoadReducer; + const orgCensusListReducer: Reducer> = genericLoadReducer; + const quadratListReducer: Reducer> = genericLoadReducer; + const siteListReducer: Reducer> = genericLoadReducer; - const [orgCensusList, orgCensusListDispatch] = useReducer>>(genericLoadReducer, []); + const [plotList, plotListDispatch] = useReducer(plotListReducer, []); - const [quadratList, quadratListDispatch] = useReducer>>(genericLoadReducer, []); + const [orgCensusList, orgCensusListDispatch] = useReducer(orgCensusListReducer, []); - const [siteList, siteListDispatch] = useReducer>>(genericLoadReducer, []); + const [quadratList, quadratListDispatch] = useReducer(quadratListReducer, []); + + const [siteList, siteListDispatch] = useReducer(siteListReducer, []); const [firstLoad, firstLoadDispatch] = useReducer(firstLoadReducer, true); diff --git a/frontend/components/client/codeeditor.tsx b/frontend/components/client/codeeditor.tsx new file mode 100644 index 00000000..3e99801e --- /dev/null +++ b/frontend/components/client/codeeditor.tsx @@ -0,0 +1,66 @@ +'use client'; + +import React, { Dispatch, SetStateAction, useRef } from 'react'; +import { Box } from '@mui/joy'; +import { useCodeMirror } from '@uiw/react-codemirror'; +import { basicSetup } from 'codemirror'; +import { sql } from '@codemirror/lang-sql'; +import { autocompletion, CompletionContext } from '@codemirror/autocomplete'; + +type CodeEditorProps = { + value: string; + setValue: Dispatch> | undefined; + schemaDetails?: { table_name: string; column_name: string }[]; + height?: string | number; + isDarkMode?: boolean; + readOnly?: boolean; +}; + +const CodeEditor: React.FC = ({ value, setValue, schemaDetails = [], height = '60vh', isDarkMode = false, readOnly = false }) => { + const editorContainerRef = useRef(null); + + // Autocomplete Suggestions + const autocompleteExtension = autocompletion({ + override: [ + (context: CompletionContext) => { + const word = context.matchBefore(/\w*/); + if (!word || word.from === word.to) return null; + + const suggestions = [ + ...Array.from(new Set(schemaDetails.map(row => row.table_name))).map(table => ({ + label: table, + type: 'keyword', + detail: 'Table', + apply: table + })), + ...schemaDetails.map(({ table_name, column_name }) => ({ + label: `${table_name}.${column_name}`, + type: 'property', + detail: `Column from ${table_name}`, + apply: `${table_name}.${column_name}` + })) + ]; + + return { + from: word.from, + options: suggestions + }; + } + ] + }); + + // Initialize CodeMirror + useCodeMirror({ + container: editorContainerRef.current, + value, + height: height.toString(), + extensions: [basicSetup, sql(), autocompleteExtension], + theme: isDarkMode ? 'dark' : 'light', + onChange: setValue, + readOnly + }); + + return ; +}; + +export default CodeEditor; diff --git a/frontend/components/client/custommonacoeditor.tsx b/frontend/components/client/custommonacoeditor.tsx deleted file mode 100644 index 04b02bc6..00000000 --- a/frontend/components/client/custommonacoeditor.tsx +++ /dev/null @@ -1,130 +0,0 @@ -'use client'; - -import * as monaco from 'monaco-editor'; -import React, { Dispatch, memo, SetStateAction, useEffect, useRef, useState } from 'react'; -import { Box, Button, Snackbar } from '@mui/joy'; - -type CustomMonacoEditorProps = { - schemaDetails: { - table_name: string; - column_name: string; - }[]; - setContent?: Dispatch>; - content?: string; - height?: string | number; - isDarkMode?: boolean; - options?: monaco.editor.IStandaloneEditorConstructionOptions; -}; - -function processExplainOutput(explainRows: any[]): { valid: boolean; message: string } { - for (const row of explainRows) { - if (row.Extra?.includes('Impossible WHERE')) { - return { valid: false, message: 'The WHERE clause is impossible to satisfy.' }; - } - - if (!row.table) { - return { valid: false, message: 'Invalid table reference.' }; - } - - if (!row.key && row.possible_keys) { - return { valid: false, message: `No indexes are being used for table ${row.table}. Consider adding an index.` }; - } - } - - return { valid: true, message: 'The query is valid.' }; -} - -function CustomMonacoEditor({ schemaDetails, setContent = () => {}, content = '', height = '60vh', options = {}, isDarkMode }: CustomMonacoEditorProps) { - const editorRef = useRef(null); - const editorContainerRef = useRef(null); - const [snackbarContent, setSnackbarContent] = useState<{ valid: boolean; message: string } | undefined>(); - const [openSnackbar, setOpenSnackbar] = useState(false); - - async function validateQuery() { - const response = await fetch(`/api/runquery`, { method: 'POST', body: JSON.stringify('EXPLAIN ' + content) }); - const data = await response.json(); - const { valid, message } = processExplainOutput(data); - - setSnackbarContent({ valid, message }); - setOpenSnackbar(true); - } - - useEffect(() => { - if (editorContainerRef.current) { - editorRef.current = monaco.editor.create(editorContainerRef.current, { - value: content, - language: 'mysql', - theme: isDarkMode ? 'vs-dark' : 'light', - ...options - }); - - editorRef.current.onDidChangeModelContent(() => { - setContent(editorRef.current?.getValue() ?? ''); - }); - - monaco.languages.registerCompletionItemProvider('mysql', { - provideCompletionItems: (model, position) => { - const suggestions: monaco.languages.CompletionItem[] = []; - 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 }; - } - }); - } - - return () => { - editorRef.current?.dispose(); - }; - }, [schemaDetails, isDarkMode]); - - return ( - <> - - -
- - setOpenSnackbar(false)} size="sm" variant="soft"> - Dismiss - - } - onClose={() => { - setSnackbarContent(undefined); - }} - > - Query validation results: {snackbarContent?.valid ? 'Valid' : 'Invalid'}
- Details: {snackbarContent?.message} -
- - ); -} - -export default memo(CustomMonacoEditor); diff --git a/frontend/components/client/postvalidationrow.tsx b/frontend/components/client/postvalidationrow.tsx index 7a32c22d..003615e1 100644 --- a/frontend/components/client/postvalidationrow.tsx +++ b/frontend/components/client/postvalidationrow.tsx @@ -9,6 +9,7 @@ import { Done } from '@mui/icons-material'; import moment from 'moment/moment'; import { darken } from '@mui/system'; import dynamic from 'next/dynamic'; +import CodeEditor from '@/components/client/codeeditor'; interface PostValidationRowProps { postValidation: PostValidationQueriesRDS; @@ -36,7 +37,7 @@ const PostValidationRow: React.FC = ({ schemaDetails }) => { const formattedResults = JSON.stringify(JSON.parse(postValidation.lastRunResult ?? '{}'), null, 2); - const CustomMonacoEditor = dynamic(() => import('@/components/client/custommonacoeditor'), { ssr: false }); + const CustomMonacoEditor = dynamic(() => import('@/components/client/codeeditor'), { 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); @@ -120,21 +121,15 @@ const PostValidationRow: React.FC = ({ }} > {expandedQuery === postValidation.queryID ? ( - + value={postValidation.queryDefinition!.replace(/\${(.*?)}/g, (_match: any, p1: string) => String(replacements[p1 as keyof typeof replacements] ?? '') )} - setContent={undefined} + setValue={undefined} height={`${Math.min(300, 20 * (postValidation?.queryDefinition ?? '').split('\n').length)}px`} isDarkMode={isDarkMode} - options={{ - readOnly: true, - minimap: { enabled: false }, - scrollBeyondLastLine: false, - wordWrap: 'off', - lineNumbers: 'off' - }} + readOnly={true} /> ) : (