From ca0512c35ff54c8e2a69f1f129f7f2d6f3c5c4ea Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Wed, 20 Mar 2024 12:46:29 +0100 Subject: [PATCH] feat: Update Project List (#443) * refactor: update list_all_projects API * refactor: clean up Project types and API calls * refactor: update Project List style and add link to PR * fix: small style and logic fixes on frontend * fix: respond comments Co-authored-by: Charles Perier <82757576+perierc@users.noreply.github.com> --------- Co-authored-by: Charles Perier <82757576+perierc@users.noreply.github.com> --- backend/editor/api.py | 9 +- backend/editor/controllers/node_controller.py | 15 +- .../editor/controllers/project_controller.py | 10 + backend/editor/entries.py | 17 +- backend/editor/models/project_models.py | 3 + backend/openapi/openapi.json | 41 ++-- taxonomy-editor-frontend/package-lock.json | 63 +++++++ taxonomy-editor-frontend/package.json | 1 + .../src/backend-types/types.ts | 38 ---- .../src/client/models/Project.ts | 9 +- .../src/client/services/DefaultService.ts | 17 +- .../src/pages/go-to-project/index.tsx | 177 ++++++++---------- .../src/pages/project/root-nodes/index.tsx | 31 ++- 13 files changed, 226 insertions(+), 205 deletions(-) diff --git a/backend/editor/api.py b/backend/editor/api.py index aa4e065b..dd2e9371 100644 --- a/backend/editor/api.py +++ b/backend/editor/api.py @@ -145,14 +145,11 @@ async def pong(response: Response): @app.get("/projects") -async def list_all_projects(response: Response, status: Optional[ProjectStatus] = None): +async def get_all_projects() -> list[Project]: """ - List projects created in the Taxonomy Editor with a status filter + List projects created in the Taxonomy Editor """ - # Listing all projects doesn't require a taxonomy name or branch name - taxonomy = TaxonomyGraph("", "") - result = await taxonomy.list_projects(status) - return result + return await project_controller.get_all_projects() @app.get("/{taxonomy_name}/{branch}/project") diff --git a/backend/editor/controllers/node_controller.py b/backend/editor/controllers/node_controller.py index 29d871f6..879e89b4 100644 --- a/backend/editor/controllers/node_controller.py +++ b/backend/editor/controllers/node_controller.py @@ -1,7 +1,7 @@ from openfoodfacts_taxonomy_parser import utils as parser_utils from ..graph_db import get_current_transaction -from ..models.node_models import EntryNodeCreate +from ..models.node_models import EntryNodeCreate, ErrorNode async def delete_project_nodes(project_id: str): @@ -43,3 +43,16 @@ async def create_entry_node( result = await get_current_transaction().run(query, params) return (await result.data())[0]["n.id"] + + +async def get_error_node(project_id: str) -> ErrorNode | None: + query = """ + MATCH (n:ERRORS {id: $project_id}) + RETURN n + """ + params = {"project_id": project_id} + result = await get_current_transaction().run(query, params) + error_node = await result.single() + if error_node is None: + return None + return ErrorNode(**error_node["n"]) diff --git a/backend/editor/controllers/project_controller.py b/backend/editor/controllers/project_controller.py index 745e3720..716aaff8 100644 --- a/backend/editor/controllers/project_controller.py +++ b/backend/editor/controllers/project_controller.py @@ -34,6 +34,16 @@ async def get_projects_by_status(status: ProjectStatus) -> list[Project]: return [Project(**record["p"]) async for record in result] +async def get_all_projects() -> list[Project]: + query = """ + MATCH (p:PROJECT) + RETURN p + ORDER BY p.created_at DESC + """ + result = await get_current_transaction().run(query) + return [Project(**record["p"]) async for record in result] + + async def create_project(project: ProjectCreate): """ Create project diff --git a/backend/editor/entries.py b/backend/editor/entries.py index 857d7fdc..c309020c 100644 --- a/backend/editor/entries.py +++ b/backend/editor/entries.py @@ -17,7 +17,7 @@ from openfoodfacts_taxonomy_parser import utils as parser_utils from . import settings, utils -from .controllers.node_controller import create_entry_node +from .controllers.node_controller import create_entry_node, get_error_node from .controllers.project_controller import create_project, edit_project, get_project from .exceptions import GithubBranchExistsError # Custom exceptions from .exceptions import ( @@ -132,11 +132,20 @@ async def get_and_parse_taxonomy(self, uploadfile: UploadFile | None = None): ) await run_in_threadpool(self.parse_taxonomy, filepath) async with TransactionCtx(): - await edit_project(self.project_name, ProjectEdit(status=ProjectStatus.OPEN)) + error_node = await get_error_node(self.project_name) + errors_count = len(error_node.errors) if error_node else 0 + await edit_project( + self.project_name, + ProjectEdit(status=ProjectStatus.OPEN, errors_count=errors_count), + ) except Exception as e: - # add an error node so we can display it with errors in the app async with TransactionCtx(): - await edit_project(self.project_name, ProjectEdit(status=ProjectStatus.FAILED)) + error_node = await get_error_node(self.project_name) + errors_count = len(error_node.errors) if error_node else 0 + await edit_project( + self.project_name, + ProjectEdit(status=ProjectStatus.FAILED, errors_count=errors_count), + ) log.exception(e) raise e diff --git a/backend/editor/models/project_models.py b/backend/editor/models/project_models.py index a50af34d..6ee7e001 100644 --- a/backend/editor/models/project_models.py +++ b/backend/editor/models/project_models.py @@ -25,13 +25,16 @@ class ProjectCreate(BaseModel): class Project(ProjectCreate): + owner_name: str | None = None created_at: DateTime + errors_count: int = 0 github_checkout_commit_sha: str | None = None github_file_latest_sha: str | None = None github_pr_url: str | None = None class ProjectEdit(BaseModel): + errors_count: int | None = None status: ProjectStatus | None = None github_checkout_commit_sha: str | None = None github_file_latest_sha: str | None = None diff --git a/backend/openapi/openapi.json b/backend/openapi/openapi.json index 4619b6cb..563b6438 100644 --- a/backend/openapi/openapi.json +++ b/backend/openapi/openapi.json @@ -32,33 +32,19 @@ }, "/projects": { "get": { - "summary": "List All Projects", - "description": "List projects created in the Taxonomy Editor with a status filter", - "operationId": "list_all_projects_projects_get", - "parameters": [ - { - "name": "status", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { "$ref": "#/components/schemas/ProjectStatus" }, - { "type": "null" } - ], - "title": "Status" - } - } - ], + "summary": "Get All Projects", + "description": "List projects created in the Taxonomy Editor", + "operationId": "get_all_projects_projects_get", "responses": { "200": { "description": "Successful Response", - "content": { "application/json": { "schema": {} } } - }, - "422": { - "description": "Validation Error", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + "schema": { + "items": { "$ref": "#/components/schemas/Project" }, + "type": "array", + "title": "Response Get All Projects Projects Get" + } } } } @@ -1248,13 +1234,21 @@ "taxonomyName": { "type": "string", "title": "Taxonomyname" }, "branchName": { "type": "string", "title": "Branchname" }, "description": { "type": "string", "title": "Description" }, - "ownerName": { "type": "string", "title": "Ownername" }, + "ownerName": { + "anyOf": [{ "type": "string" }, { "type": "null" }], + "title": "Ownername" + }, "isFromGithub": { "type": "boolean", "title": "Isfromgithub" }, "createdAt": { "type": "string", "format": "date-time", "title": "Createdat" }, + "errorsCount": { + "type": "integer", + "title": "Errorscount", + "default": 0 + }, "githubCheckoutCommitSha": { "anyOf": [{ "type": "string" }, { "type": "null" }], "title": "Githubcheckoutcommitsha" @@ -1274,7 +1268,6 @@ "taxonomyName", "branchName", "description", - "ownerName", "isFromGithub", "createdAt" ], diff --git a/taxonomy-editor-frontend/package-lock.json b/taxonomy-editor-frontend/package-lock.json index 83856b83..d740a7fa 100644 --- a/taxonomy-editor-frontend/package-lock.json +++ b/taxonomy-editor-frontend/package-lock.json @@ -13,6 +13,7 @@ "@material-table/core": "^6.2.11", "@mui/icons-material": "^5.15.10", "@mui/material": "^5.14.20", + "@mui/x-data-grid": "^6.19.6", "@tanstack/react-query": "^5.17.9", "@vitejs/plugin-react": "^4.2.1", "@yaireo/dragsort": "^1.3.1", @@ -4035,6 +4036,39 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/@mui/x-data-grid": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.19.6.tgz", + "integrity": "sha512-jpZkX1Gnlo87gKcD10mKMY8YoAzUD8Cv3/IvedH3FINDKO3hnraMeOciKDeUk0tYSj8RUDB02kpTHCM8ojLVBA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.16", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-data-grid/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@mui/x-date-pickers": { "version": "5.0.20", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.20.tgz", @@ -19149,6 +19183,11 @@ "dev": true, "peer": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -25307,6 +25346,25 @@ } } }, + "@mui/x-data-grid": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.19.6.tgz", + "integrity": "sha512-jpZkX1Gnlo87gKcD10mKMY8YoAzUD8Cv3/IvedH3FINDKO3hnraMeOciKDeUk0tYSj8RUDB02kpTHCM8ojLVBA==", + "requires": { + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.16", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "dependencies": { + "clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" + } + } + }, "@mui/x-date-pickers": { "version": "5.0.20", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.20.tgz", @@ -36488,6 +36546,11 @@ "dev": true, "peer": true }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", diff --git a/taxonomy-editor-frontend/package.json b/taxonomy-editor-frontend/package.json index 73a0e586..9273ed4e 100644 --- a/taxonomy-editor-frontend/package.json +++ b/taxonomy-editor-frontend/package.json @@ -9,6 +9,7 @@ "@material-table/core": "^6.2.11", "@mui/icons-material": "^5.15.10", "@mui/material": "^5.14.20", + "@mui/x-data-grid": "^6.19.6", "@tanstack/react-query": "^5.17.9", "@vitejs/plugin-react": "^4.2.1", "@yaireo/dragsort": "^1.3.1", diff --git a/taxonomy-editor-frontend/src/backend-types/types.ts b/taxonomy-editor-frontend/src/backend-types/types.ts index b27d1fd6..9d668d65 100644 --- a/taxonomy-editor-frontend/src/backend-types/types.ts +++ b/taxonomy-editor-frontend/src/backend-types/types.ts @@ -1,31 +1,3 @@ -type ProjectType = { - branch_name: string; - created_at: { - _DateTime__date: { - _Date__ordinal: number; - _Date__year: number; - _Date__month: number; - _Date__day: number; - }; - _DateTime__time: { - _Time__ticks: number; - _Time__hour: number; - _Time__minute: number; - _Time__second: number; - _Time__nanosecond: number; - _Time__tzinfo: any; - }; - }; - description: string; - id: string; - taxonomy_name: string; - owner_name: string; - errors_count: number; - status: string; -}; - -export type ProjectsAPIResponse = ProjectType[]; - type NodeType = { id: string; string: string | Array; @@ -36,13 +8,3 @@ export type RootEntriesAPIResponse = Array; export type SearchAPIResponse = string[]; export type ParentsAPIResponse = string[]; - -export type ProjectInfoAPIResponse = ProjectType; - -export enum ProjectStatus { - FAILED = "FAILED", - OPEN = "OPEN", - LOADING = "LOADING", - EXPORTED = "EXPORTED", - CLOSED = "CLOSED", -} diff --git a/taxonomy-editor-frontend/src/client/models/Project.ts b/taxonomy-editor-frontend/src/client/models/Project.ts index d7fd5a86..47e12fef 100644 --- a/taxonomy-editor-frontend/src/client/models/Project.ts +++ b/taxonomy-editor-frontend/src/client/models/Project.ts @@ -9,10 +9,11 @@ export type Project = { taxonomyName: string; branchName: string; description: string; - ownerName: string; + ownerName: string | null; isFromGithub: boolean; createdAt: string; - githubCheckoutCommitSha?: string | null; - githubFileLatestSha?: string | null; - githubPrUrl?: string | null; + errorsCount: number; + githubCheckoutCommitSha: string | null; + githubFileLatestSha: string | null; + githubPrUrl: string | null; }; diff --git a/taxonomy-editor-frontend/src/client/services/DefaultService.ts b/taxonomy-editor-frontend/src/client/services/DefaultService.ts index 2e49bfc4..019b74d3 100644 --- a/taxonomy-editor-frontend/src/client/services/DefaultService.ts +++ b/taxonomy-editor-frontend/src/client/services/DefaultService.ts @@ -37,24 +37,15 @@ export class DefaultService { }); } /** - * List All Projects - * List projects created in the Taxonomy Editor with a status filter - * @param status - * @returns any Successful Response + * Get All Projects + * List projects created in the Taxonomy Editor + * @returns Project Successful Response * @throws ApiError */ - public static listAllProjectsProjectsGet( - status?: ProjectStatus | null - ): CancelablePromise { + public static getAllProjectsProjectsGet(): CancelablePromise> { return __request(OpenAPI, { method: "GET", url: "/projects", - query: { - status: status, - }, - errors: { - 422: `Validation Error`, - }, }); } /** diff --git a/taxonomy-editor-frontend/src/pages/go-to-project/index.tsx b/taxonomy-editor-frontend/src/pages/go-to-project/index.tsx index 155d25ae..3ddfbe0d 100644 --- a/taxonomy-editor-frontend/src/pages/go-to-project/index.tsx +++ b/taxonomy-editor-frontend/src/pages/go-to-project/index.tsx @@ -1,66 +1,92 @@ -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Typography, Box, Grid, Link as MuiLink } from "@mui/material"; -import MaterialTable from "@material-table/core"; -import EditIcon from "@mui/icons-material/Edit"; +import { DataGrid, GridColDef, GridRowParams } from "@mui/x-data-grid"; import CircularProgress from "@mui/material/CircularProgress"; +import { useQuery } from "@tanstack/react-query"; -import useFetch from "@/components/useFetch"; -import { API_URL } from "@/constants"; import { toSnakeCase, toTitleCase } from "@/utils"; -import type { ProjectsAPIResponse } from "@/backend-types/types"; +import { DefaultService, Project, ProjectStatus } from "@/client"; -type ProjectType = { - id: string; - projectName: string; - taxonomyName: string; - ownerName: string; - branchName: string; - description: string; - errors_count: number; -}; - -export const GoToProject = () => { - const [projectData, setProjectData] = useState([]); +const ProjectsTable = ({ projects }: { projects: Project[] }) => { const navigate = useNavigate(); - const { data, isPending, isError } = useFetch( - `${API_URL}projects` - ); - - useEffect(() => { - let newProjects: ProjectType[] = []; + const columns: GridColDef[] = [ + { headerName: "Project", field: "id", flex: 3 }, + { + headerName: "Taxonomy", + field: "taxonomyName", + flex: 2, + valueFormatter: ({ value }) => toTitleCase(value), + }, + { headerName: "Branch", field: "branchName", flex: 3 }, + { headerName: "Owner", field: "ownerName", flex: 2 }, + { headerName: "Description", field: "description", flex: 3 }, + { + headerName: "Errors", + field: "errorsCount", + renderCell: ({ row }) => { + if (row.errorsCount == 0) { + return null; + } - if (data) { - const backendProjects = data.map( - ({ - id, - branch_name, - taxonomy_name, - owner_name, - description, - errors_count, - status, - }) => { - return { - id, // needed by MaterialTable as key - projectName: id, - taxonomyName: toTitleCase(taxonomy_name), - ownerName: owner_name ? owner_name : "unknown", - branchName: branch_name, - description: description, - errors_count: errors_count, - status: status, - }; + return ( + event.stopPropagation()} + > + {row.errorsCount + " errors"} + + ); + }, + }, + { + headerName: "Status", + field: "status", + renderCell: ({ row }) => { + if (row.status === ProjectStatus.EXPORTED) { + return ( + event.stopPropagation()} + > + {row.status} + + ); } - ); + }, + }, + ]; - newProjects = backendProjects; - } + const onRowClick = (params: GridRowParams) => { + navigate( + `/${toSnakeCase(params.row.taxonomyName)}/${params.row.branchName}/entry` + ); + }; + + return ( + + ); +}; - setProjectData(newProjects); - }, [data]); +export const GoToProject = () => { + const { data, isPending, isError } = useQuery({ + queryKey: ["getAllProjectsProjectsGet"], + queryFn: async () => { + return await DefaultService.getAllProjectsProjectsGet(); + }, + }); if (isError) { return ( @@ -85,57 +111,14 @@ export const GoToProject = () => { direction="column" alignItems="center" justifyContent="center" + gap={2} > List of current projects - { - if (rowData["errors_count"] > 0) { - return ( - - {rowData["errors_count"] + " errors"} - - ); - } - }, - }, - { title: "Status", field: "status" }, - ]} - options={{ - actionsColumnIndex: -1, - addRowPosition: "last", - showTitle: false, - }} - actions={[ - { - icon: () => , - tooltip: "Edit project", - onClick: (event, rowData) => { - navigate( - `/${toSnakeCase(rowData["taxonomyName"])}/${ - rowData["branchName"] - }/entry` - ); - }, - }, - ]} - /> + + + ); diff --git a/taxonomy-editor-frontend/src/pages/project/root-nodes/index.tsx b/taxonomy-editor-frontend/src/pages/project/root-nodes/index.tsx index e5a00809..b99666e5 100644 --- a/taxonomy-editor-frontend/src/pages/project/root-nodes/index.tsx +++ b/taxonomy-editor-frontend/src/pages/project/root-nodes/index.tsx @@ -22,13 +22,10 @@ import CircularProgress from "@mui/material/CircularProgress"; import CreateNodeDialogContent from "@/components/CreateNodeDialogContent"; import { toTitleCase, createBaseURL } from "@/utils"; import { greyHexCode } from "@/constants"; -import { - type ProjectInfoAPIResponse, - type RootEntriesAPIResponse, - ProjectStatus, -} from "@/backend-types/types"; +import { type RootEntriesAPIResponse } from "@/backend-types/types"; import NodesTableBody from "@/components/NodesTableBody"; import { useQuery } from "@tanstack/react-query"; +import { DefaultService, Project, ProjectStatus } from "@/client"; type RootNodesProps = { taxonomyName: string; @@ -41,7 +38,6 @@ const RootNodes = ({ taxonomyName, branchName }: RootNodesProps) => { useState(false); const baseUrl = createBaseURL(taxonomyName, branchName); - const projectInfoUrl = `${baseUrl}project`; const rootNodesUrl = `${baseUrl}rootentries`; const { @@ -49,14 +45,17 @@ const RootNodes = ({ taxonomyName, branchName }: RootNodesProps) => { isPending: infoPending, isError: infoIsError, error: infoError, - } = useQuery({ - queryKey: [projectInfoUrl], + } = useQuery({ + queryKey: [ + "getProjectInfoTaxonomyNameBranchProjectGet", + branchName, + taxonomyName, + ], queryFn: async () => { - const response = await fetch(projectInfoUrl); - if (!response.ok) { - throw new Error("Failed to fetch project info"); - } - return response.json(); + return await DefaultService.getProjectInfoTaxonomyNameBranchProjectGet( + branchName, + taxonomyName + ); }, refetchInterval: (d) => { return d.state.status === "success" && @@ -83,11 +82,7 @@ const RootNodes = ({ taxonomyName, branchName }: RootNodesProps) => { // fetch root nodes after receiving project status enabled: !!info && - [ - ProjectStatus.OPEN, - ProjectStatus.CLOSED, - ProjectStatus.EXPORTED, - ].includes(info.status as ProjectStatus), + [ProjectStatus.OPEN, ProjectStatus.EXPORTED].includes(info.status), }); let nodeIds: string[] = [];