diff --git a/.github/workflows/main-forestgeo-livesite.yml b/.github/workflows/main-forestgeo-livesite.yml index 86c5e5e2..adb22912 100644 --- a/.github/workflows/main-forestgeo-livesite.yml +++ b/.github/workflows/main-forestgeo-livesite.yml @@ -19,9 +19,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Node.js version - uses: actions/setup-node@v3 + uses: actions/setup-node@v4.0.4 with: - node-version: '18.x' + node-version: '20.x' - name: create env file (in frontend/ directory) -- production id: create-env-file-prod @@ -41,28 +41,28 @@ jobs: echo AZURE_SQL_CATALOG_SCHEMA=${{ secrets.AZURE_SQL_CATALOG_SCHEMA }} >> frontend/.env echo AZURE_STORAGE_CONNECTION_STRING=${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} >> frontend/.env echo NEXTAUTH_DEBUG=true >> frontend/.env - echo NODE_ENV=production >> frontend/.env + echo NODE_ENV=development >> frontend/.env echo PORT=3000 >> frontend/.env echo FG_PAT=${{ secrets.FG_PAT }} >> frontend/.env echo OWNER=${{ secrets.OWNER }} >> frontend/.env echo REPO=${{ secrets.REPO }} >> frontend/.env - - name: Cache node modules - uses: actions/cache@v2 - with: - path: frontend/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Cache Next.js build - uses: actions/cache@v2 - with: - path: frontend/.next/cache - key: ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/.next/cache') }} - restore-keys: | - ${{ runner.os }}-next- - ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }} +# - name: Cache node modules +# uses: actions/cache@v2 +# with: +# path: frontend/node_modules +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- +# +# - name: Cache Next.js build +# uses: actions/cache@v2 +# with: +# path: frontend/.next/cache +# key: ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/.next/cache') }} +# restore-keys: | +# ${{ runner.os }}-next- +# ${{ runner.os }}-next-${{ hashFiles('**/package-lock.json') }} - name: move into frontend --> npm install, build, and test run: | diff --git a/frontend/app/(hub)/dashboard/page.tsx b/frontend/app/(hub)/dashboard/page.tsx index 9c24b7fe..d8a32927 100644 --- a/frontend/app/(hub)/dashboard/page.tsx +++ b/frontend/app/(hub)/dashboard/page.tsx @@ -31,6 +31,7 @@ import { useEffect, useState } from 'react'; import { UnifiedChangelogRDS } from '@/config/sqlrdsdefinitions/core'; import moment from 'moment'; import Avatar from '@mui/joy/Avatar'; +import { useLoading } from '@/app/contexts/loadingprovider'; export default function DashboardPage() { const { triggerPulse, isPulsing } = useLockAnimation(); @@ -43,6 +44,8 @@ export default function DashboardPage() { const userRole = session?.user?.userStatus; const allowedSites = session?.user?.sites; + const { setLoading } = useLoading(); + const [changelogHistory, setChangelogHistory] = useState(Array(5)); const [isLoading, setIsLoading] = useState(false); diff --git a/frontend/components/client/datagridcolumns.tsx b/frontend/components/client/datagridcolumns.tsx index 3076b118..67551220 100644 --- a/frontend/components/client/datagridcolumns.tsx +++ b/frontend/components/client/datagridcolumns.tsx @@ -367,7 +367,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = standardizeGridC editable: true }, { - field: 'stemLocalX', + field: 'localX', headerName: 'X', renderHeader: () => formatHeader('X', 'Stem'), flex: 0.7, @@ -380,7 +380,7 @@ export const MeasurementsSummaryViewGridColumns: GridColDef[] = standardizeGridC filterOperators: customNumericOperators }, { - field: 'stemLocalY', + field: 'localY', headerName: 'Y', renderHeader: () => formatHeader('Y', 'Stem'), flex: 0.7, diff --git a/frontend/components/datagrids/applications/msvdatagrid.tsx b/frontend/components/datagrids/applications/msvdatagrid.tsx index a03c45f3..46379ee5 100644 --- a/frontend/components/datagrids/applications/msvdatagrid.tsx +++ b/frontend/components/datagrids/applications/msvdatagrid.tsx @@ -45,7 +45,6 @@ export default function MeasurementsSummaryViewDataGrid() { const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const currentSite = useSiteContext(); - const { setLoading } = useLoading(); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [isManualEntryFormOpen, setIsManualEntryFormOpen] = useState(false); const [triggerGlobalError, setTriggerGlobalError] = useState(false); @@ -63,6 +62,7 @@ export default function MeasurementsSummaryViewDataGrid() { }); const [isNewRowAdded, setIsNewRowAdded] = useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); + const { setLoading } = useLoading(); const addNewRowToGrid = () => { const id = randomId(); diff --git a/frontend/components/validationcard_cardmodal.tsx b/frontend/components/validationcard_cardmodal.tsx new file mode 100644 index 00000000..fec08dba --- /dev/null +++ b/frontend/components/validationcard_cardmodal.tsx @@ -0,0 +1,133 @@ +'use client'; + +import React, { useState } from 'react'; +import { Box, Button, Card, Modal, Stack, Switch, Typography } from '@mui/joy'; +import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; +import dynamic from 'next/dynamic'; + +type ValidationCardProps = { + validation: ValidationProceduresRDS; + onSaveChanges: (validation: ValidationProceduresRDS) => Promise; + onDelete: (validationID?: number) => Promise; + schemaDetails: { table_name: string; column_name: string }[]; +}; + +const ValidationCard: React.FC = ({ validation, onSaveChanges, onDelete, schemaDetails }) => { + const [isFlipped, setIsFlipped] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [scriptContent, setScriptContent] = useState(validation.definition); + const CustomMonacoEditor = dynamic(() => import('@/components/client/custommonacoeditor'), { ssr: false }); + + const handleCardClick = () => { + setIsFlipped(true); + setIsModalOpen(true); + }; + + const handleCloseModal = async () => { + setIsModalOpen(false); + setTimeout(() => setIsFlipped(false), 300); // Delay for smooth flip-back animation + }; + + const handleSaveChanges = async () => { + const updatedValidation = { ...validation, definition: scriptContent }; + await onSaveChanges(updatedValidation); + await handleCloseModal(); + }; + + return ( + + + + + + {validation.procedureName?.replace(/(DBH|HOM)([A-Z])/g, '$1 $2').replace(/([a-z])([A-Z])/g, '$1 $2')} + + + {validation.description} + + + { + const updatedValidation = { ...validation, isEnabled: e.target.checked }; + await onSaveChanges(updatedValidation); // Pass the updated object to the parent + }} + sx={{ + marginLeft: 2 + }} + onClick={e => e.stopPropagation()} + /> + + + + + + + + + + + + + + + ); +}; + +export default ValidationCard; diff --git a/frontend/components/validationcard_codemirror.tsx b/frontend/components/validationcard_codemirror.tsx new file mode 100644 index 00000000..d9b6e738 --- /dev/null +++ b/frontend/components/validationcard_codemirror.tsx @@ -0,0 +1,174 @@ +'use client'; + +import React, { useState } from 'react'; +import { Box, Button, Card, Modal, Stack, Switch, Typography, useTheme } from '@mui/joy'; +import { ValidationProceduresRDS } from '@/config/sqlrdsdefinitions/validations'; +import { basicSetup } from 'codemirror'; +import { sql } from '@codemirror/lang-sql'; +import { autocompletion, CompletionContext } from '@codemirror/autocomplete'; +import { useCodeMirror } from '@uiw/react-codemirror'; + +type ValidationCardProps = { + validation: ValidationProceduresRDS; + onSaveChanges: (validation: ValidationProceduresRDS) => Promise; + onDelete: (validationID?: number) => Promise; + schemaDetails: { table_name: string; column_name: string }[]; +}; + +const ValidationCard: React.FC = ({ validation, onSaveChanges, onDelete, schemaDetails }) => { + const [isFlipped, setIsFlipped] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [scriptContent, setScriptContent] = useState(validation.definition); + + const handleCardClick = () => { + setIsFlipped(true); + setIsModalOpen(true); + }; + + const handleCloseModal = async () => { + setIsModalOpen(false); + setTimeout(() => setIsFlipped(false), 300); + }; + + const handleSaveChanges = async () => { + const updatedValidation = { ...validation, definition: scriptContent }; + await onSaveChanges(updatedValidation); + await handleCloseModal(); + }; + + 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 + }; + } + ] + }); + const theme = useTheme(); + const isDarkMode = theme.palette.mode === 'dark'; + + const { setContainer } = useCodeMirror({ + value: scriptContent, + height: '60vh', + extensions: [basicSetup, sql(), autocompleteExtension], + theme: isDarkMode ? 'dark' : 'light', + onChange: value => setScriptContent(value) + }); + + return ( + + + + + + {validation.procedureName?.replace(/(DBH|HOM)([A-Z])/g, '$1 $2').replace(/([a-z])([A-Z])/g, '$1 $2')} + + + {validation.description} + + + { + const updatedValidation = { ...validation, isEnabled: e.target.checked }; + await onSaveChanges(updatedValidation); + }} + sx={{ + marginLeft: 2 + }} + onClick={e => e.stopPropagation()} + /> + + + + + + + + + + + + + + + ); +}; + +export default ValidationCard; diff --git a/frontend/documentation/Logging into App.md b/frontend/documentation/Logging into App.md new file mode 100644 index 00000000..3d03a0ac --- /dev/null +++ b/frontend/documentation/Logging into App.md @@ -0,0 +1,37 @@ +#login #authenticator #catalog #administrator + +### Login screen view + +![[login screen.png]] +A slideshow background will appear for about thirty seconds when you first access the website that will then fade to +back after the + +#### Logging into the app requires the following: + +1. You must have received and accepted an invitation to the SIOCIORC ForestGEO SI tenant + 1. An admin needs to perform this action with you over email +2. You'll need to set up MS Authenticator (this'll definitely take a little while, make sure you have someone to reach + out to in case of issues) +3. Your email needs to be added to the Azure MySQL catalog database + 1. This is another layer of security -- your information needs to be entered and you need to be assigned a site and + admin status (if applicable) + +> [!tip] +> Something of an optional step, but your site needs to be populated with information. Manual plot/site submissions or +> editing is still not quite ready for use + +#### The actual login process itself is pretty straightforward, uses MS login. + +Once you're logged in, you'll be redirected to the dashboard page, which will be empty. The site selection box should be +the only visible selection component visible in the sidebar, and you should be able to use the button at the bottom +right of the sidebar to logout. + +![[logged in screen.png]] + +> [!tip] +> If you have administrator access to the app, you should see the (Admin) placed below the ForestGEO in the top left +> corner. +> +> > [!warning] +> > If you should have administrator access and do not see the (Admin) indicator, please contact an admin, as a database +> > error may have occurred. diff --git a/frontend/documentation/Selecting a Census.md b/frontend/documentation/Selecting a Census.md new file mode 100644 index 00000000..eba4fe92 --- /dev/null +++ b/frontend/documentation/Selecting a Census.md @@ -0,0 +1,35 @@ +### The way the census is structured in this app is a little more complicated: + +![[census dropdown.png|300]] +There are a few important things to note here, so let's use the ==Census 3== selection as a template to explain how the +census selection has been updated. +![[census 3.png|300]] +**Census 3** denotes the Plot Census Number of the census +This refers to the overall census being completed for the referenced plot +As such, this denotes the **third** census being completed for the plot **SCBI** + +We'll move from the bottom up: +As you can see, the date ranges visible for this census are organized chronologically in descending order (newest range +first) + +![[census 3 older.png|300]] +This references the first date range for this plot census number where measurements were collected. + +> [!faq] >**Why is the census organized this way?** +> +> Censuses should not be edited once they're completed! +> If you need to correct a census (by plot census number): +> +> - You should create a new date range (**opening and then closing the census**) and add your updated information. +> - If duplicates exist for your data across different censuses, they will be highlighted as such and should be ignored. + +![[census 3 newer.png|300]] +Correspondingly, this references the next date range of measurement collection for that census. + +### Censuses must be closed before new censuses can be open + +### Censuses must be open when making modifications + +Once you've picked all three, you should see the following: + +![[all picked.png]] diff --git a/frontend/documentation/Selecting a Site.md b/frontend/documentation/Selecting a Site.md new file mode 100644 index 00000000..d93dbbad --- /dev/null +++ b/frontend/documentation/Selecting a Site.md @@ -0,0 +1,24 @@ +### Selecting a site will change the core schema you are connecting to in the Azure MySQL server + +> [!tip] +> This will trigger an app refresh/reset! Please make sure you save your changes before you attempt to change sites. + +#### When you click on the site dropdown, you should see something like this: + +![[site selection dropdown.png|300]] + +##### Important notes: + +1. Allowed sites: There are a limited subset of site(s) that you will be given access to. Please contact an + administrator if you do not have access to a website you need. +2. Other Sites: This is intended to give you a view of all available sites, so that you can get access to a site if it + becomes necessary. +3. Site Deselect: This is intended as a failsafe and will clear local data. + +### Once you select a site, a loading icon will appear, and the view of the sidebar should change: + +![[site selected.png|300]] + +> [!tip] +> Please note the indicated schema -- this is intended to help debug any issues -- having a schema name to reference +> directly will speed up the process a little as we won't have to search for the schema name corresponding to the site. diff --git a/frontend/documentation/Using the Sidebar.md b/frontend/documentation/Using the Sidebar.md new file mode 100644 index 00000000..8b7e8bfa --- /dev/null +++ b/frontend/documentation/Using the Sidebar.md @@ -0,0 +1,32 @@ +#### In order to use the app, you will need to use the Sidebar + +Let's review the logged in screen, which demonstrates what you should see immediately after you've logged in: +![[logged in screen.png]] + +> [!tip] +> The application is intended to be blank when you first sign in! +> As you make selections, you'll see loading screens and additional options to navigate to -- like a +> choose-your-own-adventure! + +## Sidebar -- Core Operations + +### Selections: + +There are a series of core selections that you'll need to make before the app is actually usable: + +1. Site --> this refers to the name of the overall location where your plot or plots is located. + +> [!tip] +> For example, for Mpala, the site is named Mpala! +> +> > [!warning] +> > Please note that for sites that only have 1 plot, the plot name and the site name will be the same! + +2. Plot --> refers to the collection of censused quadrats. +3. Census --> this is a little more complicated: + Censuses have been organized by Plot Census Number (PCN), which denotes the overall census for a given plot. The + CensusID (CID) value originally used is now used as a subunit of a PCN, to denote a date range where measurements + were submitted for that PCN. + +> [!warning] +> This is an important distinction! Please make sure you keep this in mind as you use the website. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ca3ba921..31227492 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5603,10 +5603,7 @@ "possible-typed-array-names": "^1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 10" } }, "node_modules/aws-ssl-profiles": { @@ -5862,15 +5859,6 @@ "node": ">=10.16.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5973,7 +5961,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">= 10" } }, "node_modules/chainsaw": { @@ -7503,10 +7491,7 @@ "supports-color": "^7.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 10" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { @@ -8198,7 +8183,7 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 8" } }, "node_modules/globalthis": { @@ -14932,6 +14917,7 @@ "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } + }, "node_modules/vitest": { "version": "3.0.4",