diff --git a/package-lock.json b/package-lock.json index df56090..279f2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "research-front", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "research-front", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "@cincoders/cinnamon": "^1.2.0", "@mui/icons-material": "^5.4.1", "@mui/material": "^5.5.0", "@mui/x-data-grid": "^5.12.1", - "axios": "^0.24.0", + "axios": "^1.6.5", "keycloak-js": "^18.0.0", "oidc-client-ts": "^2.2.4", "react": "18.1", @@ -4655,10 +4655,26 @@ } }, "node_modules/axios": { - "version": "0.24.0", - "license": "MIT", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dependencies": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/axobject-query": { @@ -5189,7 +5205,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001477", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "funding": [ { "type": "opencollective", @@ -5203,8 +5221,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -7327,14 +7344,15 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.14.9", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -11574,6 +11592,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.8.0", "license": "MIT" diff --git a/package.json b/package.json index 7ad6bef..12e70f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "research-front", - "version": "1.0.0", + "version": "1.1.0", "description": "Research Dashboard - System to display information extracted from professors' Lattes curriculum.", "license": "MIT", "author": "CInCoders (https://cincoders.cin.ufpe.br)", @@ -24,7 +24,7 @@ "@mui/icons-material": "^5.4.1", "@mui/material": "^5.5.0", "@mui/x-data-grid": "^5.12.1", - "axios": "^0.24.0", + "axios": "^1.6.5", "keycloak-js": "^18.0.0", "oidc-client-ts": "^2.2.4", "react": "18.1", diff --git a/src/pages/ImportXml/index.tsx b/src/pages/ImportXml/index.tsx index 18e0d29..21abe47 100644 --- a/src/pages/ImportXml/index.tsx +++ b/src/pages/ImportXml/index.tsx @@ -2,70 +2,18 @@ import { DataGrid, GridColDef, ptBR } from '@mui/x-data-grid'; import { toast, useNavbar } from '@cincoders/cinnamon'; import { useState, useEffect, useCallback } from 'react'; import { Modal, Grow } from '@mui/material'; -import { XMLDiv, DataDiv, ButtonsDiv, ImportButton, CardType } from './styles'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { XMLDiv, DataDiv, ButtonsDiv, ImportButton, CardType, AnimatedRefreshButton } from './styles'; import { ImportXmlService } from '../../services/ImportXmlService'; import { renderImportStatus } from '../../components/ImportStatus'; -import ImportCard from '../../components/ImportCard'; -import { ImportXmlRows } from '../../types/Xml.d'; -const columns: GridColDef[] = [ - { - field: 'name', - headerName: 'Arquivo', - headerAlign: 'center', - align: 'center', - description: 'Nome do arquivo', - flex: 7, - }, - { - field: 'professor', - headerName: 'Professor', - headerAlign: 'center', - align: 'center', - description: 'Professor cujo XML foi importado', - flex: 11, - type: 'string', - }, - { - field: 'status', - headerName: 'Status', - renderCell: renderImportStatus, - headerAlign: 'center', - align: 'center', - description: 'Status da importação', - flex: 6, - }, - { - field: 'importTime', - headerName: 'Tempo da importação', - headerAlign: 'center', - align: 'center', - description: 'Duração de tempo que o XML foi importado', - flex: 7, - type: 'string', - }, - { - field: 'includedAt', - headerName: 'Data da importação', - headerAlign: 'center', - align: 'center', - description: 'Data que o XML foi importado', - flex: 10, - }, - { - field: 'user', - headerName: 'Usuário', - headerAlign: 'center', - align: 'center', - description: 'Usuário que realizou a importação', - flex: 10, - }, -]; +import ImportCard from '../../components/ImportCard'; +import { ImportXmlDto } from '../../types/Xml.d'; function ImportXml() { const navbar = useNavbar(); const [loading, setLoading] = useState(true); - const [rows, setRows] = useState([]); + const [rows, setRows] = useState([]); const [open, setOpen] = useState(false); const [pageState, setPageState] = useState({ page: 1, @@ -73,6 +21,8 @@ function ImportXml() { isLoading: false, pageSize: 25, }); + const [rotatingButtons, setRotatingButtons] = useState<{ [xmlId: string]: boolean }>({}); + function dateInFull(date: Date) { const fullDate = date.toLocaleString(undefined, { year: 'numeric', @@ -84,22 +34,27 @@ function ImportXml() { }); return fullDate; } + const loadPaginatedData = useCallback(async (page: number, pageSize: number) => { setRows([]); setLoading(true); + try { const { data } = await ImportXmlService.findAllImportedXmls(pageSize, page - 1); - const xmls = data.data.map((elem, i) => ({ - id: i, + + const xmls = data.data.map(elem => ({ + id: elem.id, professor: elem.professor, name: elem.name, status: elem.status, includedAt: dateInFull(new Date(elem.includedAt)), importTime: elem.importTime ? `${elem.importTime}s` : '', user: elem.user, + storedXml: elem.storedXml, })); - setPageState(currentValue => ({ ...currentValue, total: data.totalElements })); + setRows(xmls); + setPageState(currentValue => ({ ...currentValue, total: data.totalElements })); } catch { toast.error('Não foi possível carregar o histórico de importações. Tente novamente mais tarde.', { containerId: 'page', @@ -109,6 +64,106 @@ function ImportXml() { } }, []); + const handleReprocessClick = async (xmlId: string) => { + setRotatingButtons(prevState => ({ ...prevState, [xmlId]: true })); + try { + await ImportXmlService.reprocessXML(xmlId); + await loadPaginatedData(1, pageState.pageSize); + } catch (error) { + toast.error('Não foi possível reprocessar o XML. Tente novamente mais tarde.', { + containerId: 'page', + }); + } finally { + setRotatingButtons(prevState => ({ ...prevState, [xmlId]: false })); + } + }; + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: 'Arquivo', + headerAlign: 'center', + align: 'center', + description: 'Nome do arquivo', + flex: 7, + }, + { + field: 'professor', + headerName: 'Professor', + headerAlign: 'center', + align: 'center', + description: 'Professor cujo XML foi importado', + flex: 11, + type: 'string', + }, + { + field: 'status', + headerName: 'Status', + renderCell: renderImportStatus, + headerAlign: 'center', + align: 'center', + description: 'Status da importação', + flex: 6, + }, + { + field: 'importTime', + headerName: 'Tempo da importação', + headerAlign: 'center', + align: 'center', + description: 'Duração de tempo que o XML foi importado', + flex: 7, + type: 'string', + }, + { + field: 'includedAt', + headerName: 'Data da importação', + headerAlign: 'center', + align: 'center', + description: 'Data que o XML foi importado', + flex: 10, + }, + { + field: 'user', + headerName: 'Usuário', + headerAlign: 'center', + align: 'center', + description: 'Usuário que realizou a i mportação', + flex: 10, + }, + { + field: 'reload', + headerName: '', + headerAlign: 'center', + align: 'center', + flex: 2, + renderCell: params => { + const { status, storedXml, id } = params.row; + if (storedXml) { + return ( + handleReprocessClick(id)} + variant='text' + style={{ + pointerEvents: + status === 'In Progress' || status === 'Pending' || rotatingButtons[id] ? 'none' : 'auto', + }} + sx={{ + '&:hover': { + backgroundColor: 'initial', + }, + }} + disableRipple + > + + + ); + } + return null; + }, + }, + ]; + useEffect(() => { navbar?.setTitle('Importação de XML'); }, [navbar]); @@ -147,6 +202,7 @@ function ImportXml() { }, }, }} + key={pageState.page} /> diff --git a/src/pages/ImportXml/styles.ts b/src/pages/ImportXml/styles.ts index 1c4832b..1c0cabc 100644 --- a/src/pages/ImportXml/styles.ts +++ b/src/pages/ImportXml/styles.ts @@ -1,4 +1,30 @@ -import { Box, Button, Card, styled } from '@mui/material'; +import { Box, Button, Card } from '@mui/material'; +import styled, { keyframes, css } from 'styled-components'; + +interface CustomButtonProps { + isrotating: boolean; +} + +const spin = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +`; + +export const AnimatedRefreshButton = styled(Button)` + background: none; + border: none; + cursor: pointer; + + ${(props: CustomButtonProps) => + props.isrotating && + css` + animation: ${spin} 1s linear infinite; + `} +`; export const XMLDiv = styled(Box)({ display: 'flex', diff --git a/src/pages/ProfessorsList/index.tsx b/src/pages/ProfessorsList/index.tsx index 9247880..1344930 100644 --- a/src/pages/ProfessorsList/index.tsx +++ b/src/pages/ProfessorsList/index.tsx @@ -31,20 +31,65 @@ function Table() { headerName: 'NOME', headerAlign: 'center', description: 'Nome do professor', - flex: 30, + flex: 25, renderCell: params => { const link = Links.PROFESSOR_INFO.replace(':id', params.row.professorId as unknown as string); return ; }, renderHeader: renderHeaderTooltip, }, + { + field: 'computerArticles', + headerName: 'ARTIGOS', + headerAlign: 'center', + align: 'center', + description: 'Nº de artigos de computação do professor', + flex: 9, + renderHeader: renderHeaderTooltip, + }, + { + field: 'computerPublications', + headerName: 'PUBLICAÇÕES', + headerAlign: 'center', + align: 'center', + description: 'Nº de publicações de computação do professor', + flex: 8, + renderHeader: renderHeaderTooltip, + }, + { + field: 'books', + headerName: 'LIVROS', + headerAlign: 'center', + align: 'center', + description: 'Nº de livros escritos pelo professor', + flex: 8, + renderHeader: renderHeaderTooltip, + }, + { + field: 'patents', + headerName: 'PATENTES', + headerAlign: 'center', + align: 'center', + description: 'Nº de patentes feitos pelo professor', + flex: 8, + renderHeader: renderHeaderTooltip, + }, + { + field: 'artisticProductions', + headerName: 'PROD. ARTÍSTICAS', + headerAlign: 'center', + align: 'center', + description: 'Nº de produções artísticas feitas pelo professor', + flex: 8, + renderHeader: renderHeaderTooltip, + }, { field: 'idLattes', headerName: 'CÓDIGO LATTES', headerAlign: 'center', align: 'center', description: 'Código lattes do professor', - flex: 10, + flex: 15, renderCell: params => , renderHeader: renderHeaderTooltip, }, @@ -95,7 +140,7 @@ function Table() { headerAlign: 'center', align: 'center', description: 'Excluir Professor', - flex: 5, + flex: 7, minWidth: 10, renderCell: params => { const professor = params.row; @@ -124,6 +169,11 @@ function Table() { professorId: element.professorId, professorName: element.professorName, identifier: element.identifier, + artisticProductions: element.artisticProductions, + books: element.books, + computerArticles: element.computerArticles, + computerPublications: element.computerPublications, + patents: element.patents, })); setRows(newData); } else { diff --git a/src/services/ImportXmlService.ts b/src/services/ImportXmlService.ts index 3e7e0d3..c5803f1 100644 --- a/src/services/ImportXmlService.ts +++ b/src/services/ImportXmlService.ts @@ -21,7 +21,9 @@ export class ImportXmlService { }, validateStatus: (status: number) => [201, 500].includes(status), onUploadProgress: progressEvent => { - const progress: number = Math.round((progressEvent.loaded / progressEvent.total) * 100); + const progress: number = progressEvent?.total + ? Math.round((progressEvent.loaded / progressEvent.total) * 100) + : 0; updateProgress(progress); }, }); @@ -39,4 +41,11 @@ export class ImportXmlService { }); return response; } + + static async reprocessXML(id: string): Promise { + const response = await apiBack.get(`import-xml/${id}/reprocess`, { + validateStatus: (status: number) => [200].includes(status), + }); + return response; + } } diff --git a/src/types/Professor.d.ts b/src/types/Professor.d.ts index 89068bb..d7d6155 100644 --- a/src/types/Professor.d.ts +++ b/src/types/Professor.d.ts @@ -9,4 +9,9 @@ export interface ProfessorDetails { id?: number; professorName: string; identifier: string; + artisticProductions: number; + books: number; + computerArticles: number; + computerPublications: number; + patents: number; } diff --git a/src/types/Xml.d.ts b/src/types/Xml.d.ts index 6dd531b..a0258f1 100644 --- a/src/types/Xml.d.ts +++ b/src/types/Xml.d.ts @@ -16,14 +16,12 @@ export interface Pagination { } export interface ImportXmlDto { + id: string; name: string; professor: string; user: string; status: string; includedAt: string; importTime: string | undefined; -} - -export interface ImportXmlRows extends ImportXmlDto { - id: number; + storedXml: boolean; }