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

React 19 upgrade #468

Merged
merged 19 commits into from
Feb 27, 2025
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -98,10 +98,10 @@ update-deps-locks-all:
echo 20 > .nvmrc

install-node-deps: package-lock.json
${NPM_EXEC} install --omit-dev
${NPM_EXEC} install --strict-peer-deps --omit-dev

install-node-test-deps: package-lock.json
${NPM_EXEC} install
${NPM_EXEC} install --strict-peer-deps

install-node: .nvmrc
${NVM_CMD} install
@@ -110,10 +110,10 @@ update-install-node: install-node
@:

package-lock.json:
${NPM_EXEC} install --package-lock-only
${NPM_EXEC} install --strict-peer-deps --package-lock-only

update-node-deps-lock: package-lock.json
${NPM_EXEC} update --package-lock-only
${NPM_EXEC} update --strict-peer-deps --package-lock-only

_vars-node-nvm-exec:
@echo "NVM_CMD=${NVM_CMD}"
17 changes: 0 additions & 17 deletions webui/.eslintrc.json

This file was deleted.

4 changes: 2 additions & 2 deletions webui/cypress/e2e/health-endpoints.cy.ts
Original file line number Diff line number Diff line change
@@ -14,9 +14,9 @@ describe('check all health endpoints', () => {
response_cache_control = cache_control_flags
}

expect(response_cache_control).not.to.be.undefined
expect(response_cache_control).not.to.be.an('undefined')

let cache_control_map = new Map()
const cache_control_map = new Map()
response_cache_control.forEach((val) => { cache_control_map.set(val, true) })

expect(cache_control_map).to.include.any.keys('no-cache', 'no-store')
11 changes: 7 additions & 4 deletions webui/cypress/e2e/submit-workflow.cy.ts
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ describe('submit form behaviour', () => {
// cy.task("generateReport");
// });

Cypress.on('uncaught:exception', (err, runnable) => { // eslint-disable-line no-unused-vars
Cypress.on('uncaught:exception', (err, ) => {
// Expect errors from nightingale elements are ignored
// InvalidStateError: CanvasRenderingContext2D.drawImage: Passed-in canvas is empty
console.log(`Uncaught error intercepted during cypress testing.`)
@@ -68,11 +68,13 @@ describe('submit form behaviour', () => {
for(let i = 0, len = formInput.length; i < len; ++i){

// Form should be able to receive gene as user input.
cy.get('.p-inputgroup').eq(i).find('input#gene').focus().type(formInput[i].gene)
cy.get('.p-inputgroup').eq(i).find('input#gene').as('geneInputField')
cy.get('@geneInputField').focus()
cy.get('@geneInputField').type(formInput[i].gene)

// Once the transcript list loaded, from should enable selecting the relevant transcripts.
cy.get('.p-inputgroup').eq(i).find('#transcripts').find('input').focus()
cy.get('.p-multiselect-panel').as('openTranscriptsSelectBox').should('be.visible')
cy.get('.p-multiselect-panel', {timeout: 5000}).as('openTranscriptsSelectBox').should('be.visible')

// A list of transcript should be available
cy.get('@openTranscriptsSelectBox').find('li.p-multiselect-item').as('openTranscriptsList')
@@ -91,6 +93,7 @@ describe('submit form behaviour', () => {
cy.get('@submitBtn').should('be.disabled')

if (i === 0) {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.get('@submitBtn').should('be.disabled')
}
@@ -190,7 +193,7 @@ describe('submit form behaviour', () => {
cy.get('@nightingaleSequenceView').should('have.attr', 'color-scheme', defaultColorScheme)

// Give visual nightingale-elements some time to render
cy.wait(1000)
cy.wait(1000) //eslint-disable-line cypress/no-unnecessary-waiting

// Compare (visual) snapshot of successfull cypress @nightingaleSequenceView render
if( !Cypress.config('isInteractive') ) {
40 changes: 40 additions & 0 deletions webui/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pluginJest from 'eslint-plugin-jest';
import pluginCypress from 'eslint-plugin-cypress/flat';
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});

const config = [
...compat.config({
extends: ["next/core-web-vitals", "next/typescript", "eslint:recommended"],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
}
}),
{
files: ["**/__tests__/**/*.[jt]s?(x)"],

plugins: {
pluginJest,
},

languageOptions: {
globals: pluginJest.environments.globals.globals
}
}, {
files: ["cypress/**/*.cy.[jt]s?(x)"],

...pluginCypress.configs.recommended,
}
];

export default config
3 changes: 3 additions & 0 deletions webui/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
dirs: ['src', 'cypress/e2e', 'cypress/support'],
},
output: 'standalone',
skipTrailingSlashRedirect: true,
experimental: {
2,140 changes: 1,158 additions & 982 deletions webui/package-lock.json

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions webui/package.json
Original file line number Diff line number Diff line change
@@ -15,21 +15,24 @@
"node": "^20.16"
},
"dependencies": {
"@jbrowse/core": "~2.18.0",
"@lit/react": "^1.0.7",
"@nightingale-elements/nightingale-manager": "^5.0.0",
"@nightingale-elements/nightingale-msa": "^5.0.0",
"@nightingale-elements/nightingale-navigation": "^5.0.0",
"clustal-js": "^2.0.6",
"generic-sequence-panel": "^1.6.0",
"next": "~14.2.7",
"next": "~15.1.0",
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primereact": "~10.9.1",
"react": "^18",
"react-dom": "^18",
"react": "^19",
"react-dom": "^19",
"uuid": "^11.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@jest/globals": "^29.7.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
@@ -38,14 +41,14 @@
"@types/fetch-mock": "^7.3.8",
"@types/jest": "^29.5.12",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "^19",
"@types/react-dom": "^19",
"cypress": "^14.0.0",
"cypress-image-diff-html-report": "^2.2.0",
"cypress-image-diff-js": "^2.3.0",
"cypress-real-events": "^1.14.0",
"eslint-config-next": "~14.2.7",
"eslint-plugin-cypress": "^3.5.0",
"eslint-config-next": "~15.1.0",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-jest": "^28.6.0",
"fetch-mock": "^12.1.0",
"jest": "^29.7.0",
2 changes: 1 addition & 1 deletion webui/src/app/components/DarkModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { PrimeReactContext } from 'primereact/api';
import { ToggleButton } from "primereact/togglebutton";
import React, { FunctionComponent, useContext, useState } from 'react';

export const DarkModeToggle: FunctionComponent<{}> = () => {
export const DarkModeToggle: FunctionComponent = () => {
const [darkMode, setDarkMode] = useState(false)
const { changeTheme } = useContext(PrimeReactContext);

Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ describe('apiHealthHTTPStatus function testing', () => {
fetchMock.route('/api/health', mockChangingApiHealthResponse, {name: 'catch-api-health'});

const apiHealthUpResponse = await apiHealthHTTPStatus()
expect(apiHealthUpResponse).toBeDefined
expect(apiHealthUpResponse).toBeDefined()
expect(apiHealthUpResponse![0]).toBe(200)

const fetchCalls1 = fetchMock.callHistory.calls('catch-api-health')
@@ -61,7 +61,7 @@ describe('apiHealthHTTPStatus function testing', () => {

// Test second API Health query response (failure)
const apiHealthDownResponse = await apiHealthHTTPStatus()
expect(apiHealthDownResponse).toBeDefined
expect(apiHealthDownResponse).toBeDefined()
expect(apiHealthDownResponse![0]).toBe(500)

const fetchCalls2 = fetchMock.callHistory.calls('catch-api-health')
@@ -72,7 +72,7 @@ describe('apiHealthHTTPStatus function testing', () => {

// Test third API Health query response (not accessible)
const apiHealthInaccessibleResponse = await apiHealthHTTPStatus()
expect(apiHealthInaccessibleResponse).toBeUndefined
expect(apiHealthInaccessibleResponse).toBeUndefined()

const fetchCalls3 = fetchMock.callHistory.calls('catch-api-health')
expect(fetchCalls3).toHaveLength(3)
3 changes: 3 additions & 0 deletions webui/src/app/health/page.tsx
Original file line number Diff line number Diff line change
@@ -7,3 +7,6 @@ export default async function Page() {
<WebUiHealthCheck />
)
}

// Force dynamic page rendering by next.js
export const dynamic = 'force-dynamic'
2 changes: 1 addition & 1 deletion webui/src/app/progress/page.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { redirect } from 'next/navigation'

export default async function Page( props: any ) {

const searchParams: Record<string, any> = props.searchParams
const searchParams: Record<string, any> = (await props.searchParams)
const jobUuidStr = searchParams['uuid'] as string

if( !jobUuidStr ){
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ export const AlignmentResultView: FunctionComponent<AlignmentResultViewProps> =
console.log(`AlignmentResult updated to: ${alignmentResult}`)
}

}, [alignmentResult] // eslint-disable-line react-hooks/exhaustive-deps
}, [alignmentResult]
)

return (
2 changes: 1 addition & 1 deletion webui/src/app/result/page.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { redirect } from 'next/navigation'

export default async function Page( props: any ) {

const searchParams: Record<string, any> = props.searchParams
const searchParams: Record<string, any> = (await props.searchParams)
const jobUuidStr = searchParams['uuid'] as string

if( !jobUuidStr ){
20 changes: 10 additions & 10 deletions webui/src/app/submit/components/AlignmentEntry/AlignmentEntry.tsx
Original file line number Diff line number Diff line change
@@ -28,14 +28,14 @@ export interface AlignmentEntryProps {
readonly dispatchInputPayloadPart: React.Dispatch<InputPayloadDispatchAction>
}
export const AlignmentEntry: FunctionComponent<AlignmentEntryProps> = (props: AlignmentEntryProps) => {
const [setupCompleted, setSetupCompleted] = useState<Boolean>(false)
const geneMessageRef: React.RefObject<Message> = createRef();
const [setupCompleted, setSetupCompleted] = useState<boolean>(false)
const geneMessageRef: React.RefObject<Message | null> = createRef();
const [geneMessageDisplay, setgeneMessageDisplay] = useState('none')
const [gene, setGene] = useState<GeneInfo>()
const transcriptMultiselectRef: React.RefObject<MultiSelect> = createRef();
const transcriptMultiselectRef: React.RefObject<MultiSelect | null> = createRef();
const [transcriptList, setTranscriptList] = useState<Feature[]>([])
const [transcriptListFocused, setTranscriptListFocused] = useState<Boolean>(false)
const [transcriptListOpened, setTranscriptListOpened] = useState<Boolean>(false)
const [transcriptListFocused, setTranscriptListFocused] = useState<boolean>(false)
const [transcriptListOpened, setTranscriptListOpened] = useState<boolean>(false)
const [selectedTranscriptIds, setSelectedTranscriptIds] = useState<Array<any>>([])
const [transcriptListLoading, setTranscriptListLoading] = useState(true)
const [fastaFileUrl, setFastaFileUrl] = useState<string>()
@@ -101,7 +101,7 @@ export const AlignmentEntry: FunctionComponent<AlignmentEntryProps> = (props: Al
}

const payloadPortion = useCallback((gene_info: GeneInfo, transcripts_info: TranscriptInfoType[]) => {
let portion: JobSumbissionPayloadRecord[] = []
const portion: JobSumbissionPayloadRecord[] = []

transcripts_info.forEach(transcript => {
portion.push({
@@ -129,7 +129,7 @@ export const AlignmentEntry: FunctionComponent<AlignmentEntryProps> = (props: Al
const jBrowseSubfeatureRelToRefPos = (subfeatureList: any[], featureStrand: FeatureStrand,
parentRefStart: number, parentRefEnd: number) => (
subfeatureList.map((subfeat: any) => {
let new_subfeat = {
const new_subfeat = {
...subfeat,
}

@@ -146,15 +146,15 @@ export const AlignmentEntry: FunctionComponent<AlignmentEntryProps> = (props: Al
})
)

const processTranscriptEntry = useCallback(async(transcriptIds: String[]) => {
const processTranscriptEntry = useCallback(async(transcriptIds: string[]) => {
updateInputPayloadPart({
status: AlignmentEntryStatus.PROCESSING,
payloadPart: undefined
})
console.log(`selected transcripts (${transcriptIds.length}): ${transcriptIds}`)
console.log('Fetching exon info for selected transcripts...')

let transcriptsInfo: Array<TranscriptInfoType> = []
const transcriptsInfo: Array<TranscriptInfoType> = []

if(transcriptIds.length < 1){
updateInputPayloadPart({
@@ -193,7 +193,7 @@ export const AlignmentEntry: FunctionComponent<AlignmentEntryProps> = (props: Al
let exons: any[] = dedupe(children.filter((sub: any) => sub.type === 'exon'))
let cds_regions: any[] = dedupe(children.filter((sub: any) => sub.type === 'CDS'))

let transcript_length = transcript.get("end") - transcript.get("start")
const transcript_length = transcript.get("end") - transcript.get("start")
if (feature.strand === -1) {
exons = revlist(exons, transcript_length)
cds_regions = revlist(cds_regions, transcript_length)
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ export const AlignmentEntryList: FunctionComponent<AlignmentEntryListProps> = (p
const [alignmentEntries, setAlignmentEntries] = useState<Map<number, AlignmentEntryListItem>>(new Map())
function initiateFirstAlignmentEntry(){
setAlignmentEntries((prevState) => {
let newState = new Map(prevState)
const newState = new Map(prevState)
if(prevState.size === 0){
console.log('Initiating first alignmentEntry.')
const firstItemIndex = 0
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export const JobSubmitForm: FunctionComponent<JobSumbitProps> = (props: JobSumbi
console.info(`agrjBrowseDataRelease: ${props.agrjBrowseDataRelease}`)

const inputPayloadReducer = (prevState: InputPayloadPartMap, action: InputPayloadDispatchAction) => {
let newState = new Map(prevState)
const newState = new Map(prevState)
const entityIndex = action.index

switch (action.type) {
1 change: 1 addition & 0 deletions webui/tsconfig.json
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
},
"downlevelIteration": true,
"esModuleInterop": true,
"target": "ES2017"
},
"include": [
"next-env.d.ts",