diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..abdf5c6 --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,17 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": true, + "printWidth": 80, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} \ No newline at end of file diff --git a/app/src/lib/api/mapping_service/index.ts b/app/src/lib/api/mapping_service/index.ts new file mode 100644 index 0000000..35c234a --- /dev/null +++ b/app/src/lib/api/mapping_service/index.ts @@ -0,0 +1,110 @@ +import encodeFileToBase64 from '../../../utils/base64encoder'; +import ApiService from '../../services/api_service'; +import { MappingGraph } from './types'; + +class MappingService { + private static getApiClient(): ApiService { + return ApiService.getInstance('default'); + } + + public static async getMappingsInWorkspace( + workspaceUuid: string, + ): Promise { + const result = await this.getApiClient().callApi( + `/workspaces/${workspaceUuid}/mapping`, + { + method: 'GET', + parser: data => data as MappingGraph[], + }, + ); + + if (result.type === 'success') { + return result.data; + } + + throw new Error( + `Failed to get mappings: ${result.message} (status: ${result.status})`, + ); + } + + public static async createMappingInWorkspace( + workspaceUuid: string, + name: string, + description: string, + content: File, + sourceType: 'csv' | 'json', + extra: Record, + ): Promise { + const base64EncodedFile = await encodeFileToBase64(content); + const data = { + name, + description, + content: base64EncodedFile, + source_type: sourceType, + extra, + }; + + const result = await this.getApiClient().callApi( + `/workspaces/${workspaceUuid}/mapping`, + { + method: 'POST', + body: data, + parser: () => true, + }, + ); + + if (result.type === 'success') { + return result.data; + } + + throw new Error( + `Failed to create mapping: ${result.message} (status: ${result.status})`, + ); + } + + public static async deleteMappingInWorkspace( + workspaceUuid: string, + mappingUuid: string, + ): Promise { + const result = await this.getApiClient().callApi( + `/workspaces/${workspaceUuid}/mapping/${mappingUuid}`, + { + method: 'DELETE', + parser: () => true, + }, + ); + + if (result.type === 'success') { + return result.data; + } + + throw new Error( + `Failed to delete mapping: ${result.message} (status: ${result.status})`, + ); + } + + public static async updateMapping( + workspaceUuid: string, + mappingUuid: string, + data: MappingGraph, + ): Promise { + const result = await this.getApiClient().callApi( + `/workspaces/${workspaceUuid}/mapping/${mappingUuid}`, + { + method: 'PUT', + body: data, + parser: () => true, + }, + ); + + if (result.type === 'success') { + return result.data; + } + + throw new Error( + `Failed to update mapping: ${result.message} (status: ${result.status})`, + ); + } +} + +export default MappingService; diff --git a/app/src/lib/api/mapping_service/types.ts b/app/src/lib/api/mapping_service/types.ts new file mode 100644 index 0000000..3493f8e --- /dev/null +++ b/app/src/lib/api/mapping_service/types.ts @@ -0,0 +1,43 @@ +export enum MappingNodeType { + ENTITY = 'entity', + LITERAL = 'literal', + URIRef = 'uri_ref', +} + +export interface MappingNode { + id: string; + type: MappingNodeType.ENTITY; + label: string; + uri_pattern: string; + rdf_type: string[]; +} + +export interface MappingLiteral { + id: string; + type: MappingNodeType.LITERAL; + label: string; + value: string; + literal_type: string; +} + +export interface MappingURIRef { + id: string; + type: MappingNodeType.URIRef; + uri_pattern: string; +} + +export interface MappingEdge { + id: string; + source: string; + target: string; + predicate_uri: string; +} + +export interface MappingGraph { + uuid: string; + name: string; + description: string; + source_id: string; + nodes: (MappingNode | MappingLiteral | MappingURIRef)[]; + edges: MappingEdge[]; +} diff --git a/app/src/pages/prefixes_page/components/PrefixCardItem/index.tsx b/app/src/pages/prefixes_page/components/PrefixCardItem/index.tsx index e7a2095..a89f923 100644 --- a/app/src/pages/prefixes_page/components/PrefixCardItem/index.tsx +++ b/app/src/pages/prefixes_page/components/PrefixCardItem/index.tsx @@ -12,18 +12,14 @@ const PrefixCardItem = ({ prefix, onDelete }: PrefixCardItemProps) => { -

- URI: {prefix.uri} -

- +

+ URI: {prefix.uri} +

} actions={ - <> - - + } /> ); diff --git a/app/src/pages/workspace_page/components/MappingCard/index.tsx b/app/src/pages/workspace_page/components/MappingCard/index.tsx new file mode 100644 index 0000000..de620bc --- /dev/null +++ b/app/src/pages/workspace_page/components/MappingCard/index.tsx @@ -0,0 +1,34 @@ +import { Button } from '@blueprintjs/core'; +import CardItem from '../../../../components/CardItem'; +import { MappingGraph } from '../../../../lib/api/mapping_service/types'; + +interface MappingCardItemProps { + mapping: MappingGraph; + onSelected: (mapping: MappingGraph) => void; + onDelete: (mapping: MappingGraph) => void; +} + +const MappingCardItem = ({ + mapping, + onSelected, + onDelete, +}: MappingCardItemProps) => { + return ( + + Description: {mapping.description} +

+ } + actions={ + <> + + + + } + /> + ); +}; + +export default MappingCardItem; diff --git a/app/src/pages/workspace_page/index.tsx b/app/src/pages/workspace_page/index.tsx index a57be51..16aaa21 100644 --- a/app/src/pages/workspace_page/index.tsx +++ b/app/src/pages/workspace_page/index.tsx @@ -1,9 +1,10 @@ -import { Button, ButtonGroup, Navbar } from '@blueprintjs/core'; +import { Button, ButtonGroup, Navbar, NonIdealState } from '@blueprintjs/core'; import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import useWorkspacePageState from './state'; import useErrorToast from '../../hooks/useErrorToast'; +import MappingCardItem from './components/MappingCard'; import './styles.scss'; type WorkspacePageURLProps = { @@ -14,6 +15,7 @@ const WorkspacePage = () => { const props = useParams(); const navigation = useNavigate(); const workspace = useWorkspacePageState(state => state.workspace); + const mappingGraphs = useWorkspacePageState(state => state.mappingGraphs); const isLoading = useWorkspacePageState(state => state.isLoading); const error = useWorkspacePageState(state => state.error); const loadWorkspace = useWorkspacePageState(state => state.loadWorkspace); @@ -64,6 +66,37 @@ const WorkspacePage = () => { +
+ {!workspace && <>} + {workspace && mappingGraphs.length === 0 && ( + navigation('create')} + > + Create New Mapping + + } + /> + )} + {workspace && mappingGraphs.length > 0 && ( +
+ {mappingGraphs.map(mappingGraph => ( + {}} + onSelected={() => {}} + /> + ))} +
+ )} +
); }; diff --git a/app/src/pages/workspace_page/state.ts b/app/src/pages/workspace_page/state.ts index 11e49e3..801828f 100644 --- a/app/src/pages/workspace_page/state.ts +++ b/app/src/pages/workspace_page/state.ts @@ -2,21 +2,34 @@ import { Workspace } from '../../lib/api/workspaces_api/types'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; +import MappingService from '../../lib/api/mapping_service'; +import { MappingGraph } from '../../lib/api/mapping_service/types'; import WorkspacesApi from '../../lib/api/workspaces_api'; import { ZustandActions } from '../../utils/zustand'; interface WorkspacePageState { workspace: Workspace | null; + mappingGraphs: MappingGraph[]; isLoading: string | null; // If the value is null, it means that the data is not being loaded, otherwise it will contain the loading message error: string | null; } interface WorkspacePageStateActions { - loadWorkspace: (uuid: string) => void; + loadWorkspace: (uuid: string) => Promise; + createMapping: ( + workspaceUuid: string, + name: string, + description: string, + content: File, + sourceType: 'csv' | 'json', + extra: Record, + ) => Promise; + deleteMapping: (workspaceUuid: string, mappingUuid: string) => Promise; } const defaultState: WorkspacePageState = { workspace: null, + mappingGraphs: [], isLoading: null, error: null, }; @@ -25,20 +38,66 @@ const functions: ZustandActions< WorkspacePageStateActions, WorkspacePageState > = set => ({ - loadWorkspace(uuid) { + async loadWorkspace(uuid) { set({ isLoading: 'Loading workspace...' }); - WorkspacesApi.getWorkspace(uuid) - .then(workspace => { - set({ workspace, error: null }); - }) - .catch(error => { - if (error instanceof Error) { - set({ error: error.message }); - } - }) - .finally(() => { - set({ isLoading: null }); - }); + try { + const workspacePromise = WorkspacesApi.getWorkspace(uuid); + const mappingGraphsPromise = await MappingService.getMappingsInWorkspace( + uuid, + ); + // Wait for all the promises to resolve + const [workspace, mappingGraphs] = await Promise.all([ + workspacePromise, + mappingGraphsPromise, + ]); + set({ workspace, mappingGraphs, error: null }); + } catch (error) { + if (error instanceof Error) { + set({ error: error.message }); + } + } finally { + set({ isLoading: null }); + } + }, + async createMapping( + workspaceUuid, + name, + description, + content, + sourceType, + extra, + ) { + set({ isLoading: 'Creating mapping...' }); + try { + await MappingService.createMappingInWorkspace( + workspaceUuid, + name, + description, + content, + sourceType, + extra, + ); + await this.loadWorkspace(workspaceUuid); + } catch (error) { + if (error instanceof Error) { + set({ error: error.message }); + } + } finally { + set({ isLoading: null, error: null }); + } + }, + async deleteMapping(workspaceUuid, mappingUuid) { + set({ isLoading: 'Deleting mapping...' }); + try { + await MappingService.deleteMappingInWorkspace(workspaceUuid, mappingUuid); + await this.loadWorkspace(workspaceUuid); + } catch (error) { + if (error instanceof Error) { + set({ error: error.message }); + } + } finally { + set({ isLoading: null, error: null }); + } }, }); diff --git a/app/src/pages/workspace_page/styles.scss b/app/src/pages/workspace_page/styles.scss index 8d16404..c247f7e 100644 --- a/app/src/pages/workspace_page/styles.scss +++ b/app/src/pages/workspace_page/styles.scss @@ -6,3 +6,9 @@ width: 100vw; height: 100vh; } + +.workspace-page-content { + margin-top: bp-vars.$pt-navbar-height; + width: 100vw; + height: calc(100vh - #{bp-vars.$pt-navbar-height}); +} diff --git a/bootstrap.py b/bootstrap.py index 8887c25..25d4139 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -43,6 +43,8 @@ async def bootstrap(): f"Application directory set to {di['APP_DIR']}" ) + di["TEMP_DIR"] = di["APP_DIR"] / "temp" + # Detecting system and architecture for later use di["SYSTEM"] = platform.system() di["ARCH"] = platform.machine() diff --git a/server/routers/workspaces/workspaces.py b/server/routers/workspaces/workspaces.py index 33d16ca..30f37f5 100644 --- a/server/routers/workspaces/workspaces.py +++ b/server/routers/workspaces/workspaces.py @@ -46,6 +46,7 @@ GetPrefixInWorkspaceFacade, ) from server.models.mapping import MappingGraph +from server.models.ontology import Ontology from server.models.workspace import WorkspaceModel from server.routers.models import BasicResponse from server.routers.workspaces.models import ( @@ -293,7 +294,7 @@ async def delete_prefix( async def get_ontologies( workspace_id: str, get_ontology_in_workspace_facade: GetOntologyInWorkspaceFacadeDep, -) -> list[dict]: +) -> list[Ontology]: facade_response = ( get_ontology_in_workspace_facade.execute( workspace_id=workspace_id, diff --git a/server/service_protocols/mapping_service_protocol/__init__.py b/server/service_protocols/mapping_service_protocol/__init__.py index 95dceea..73189d4 100644 --- a/server/service_protocols/mapping_service_protocol/__init__.py +++ b/server/service_protocols/mapping_service_protocol/__init__.py @@ -58,13 +58,3 @@ def delete_mapping(self, mapping_id: str) -> None: None """ pass - - @abstractmethod - def list_mappings(self) -> list[str]: - """ - List all mappings - - Returns: - list[str]: List of mapping IDs - """ - pass diff --git a/server/utils/schema_extractor/__init__.py b/server/utils/schema_extractor/__init__.py index 5bf9005..f65062c 100644 --- a/server/utils/schema_extractor/__init__.py +++ b/server/utils/schema_extractor/__init__.py @@ -1,12 +1,12 @@ from kink import inject -from utils.schema_extractor.i_schema_extractor import ( +from server.utils.schema_extractor.i_schema_extractor import ( ISchemaExtractor, ) -from utils.schema_extractor.json_schema_extractor import ( +from server.utils.schema_extractor.json_schema_extractor import ( JSONSchemaExtractor, # noqa: F401 ) -from utils.schema_extractor.tabular_schema_extractor import ( +from server.utils.schema_extractor.tabular_schema_extractor import ( TabularSchemaExtractor, # noqa: F401 ) diff --git a/server/utils/schema_extractor/json_schema_extractor.py b/server/utils/schema_extractor/json_schema_extractor.py index ad8ed69..c6578b3 100644 --- a/server/utils/schema_extractor/json_schema_extractor.py +++ b/server/utils/schema_extractor/json_schema_extractor.py @@ -2,7 +2,8 @@ from io import BytesIO from kink import inject -from utils.schema_extractor.i_schema_extractor import ( + +from server.utils.schema_extractor.i_schema_extractor import ( ISchemaExtractor, ) @@ -11,7 +12,7 @@ class JSONSchemaExtractor(ISchemaExtractor): def __init__( self, - temp_storage: str, + TEMP_DIR: str, ): super().__init__("JSON Schema Extractor", ["json"]) diff --git a/server/utils/schema_extractor/tabular_schema_extractor.py b/server/utils/schema_extractor/tabular_schema_extractor.py index 9038d10..0ad13d2 100644 --- a/server/utils/schema_extractor/tabular_schema_extractor.py +++ b/server/utils/schema_extractor/tabular_schema_extractor.py @@ -1,9 +1,9 @@ from io import BytesIO -from kink import inject import pandas as pd +from kink import inject -from utils.schema_extractor.i_schema_extractor import ( +from server.utils.schema_extractor.i_schema_extractor import ( ISchemaExtractor, ) @@ -12,7 +12,7 @@ class TabularSchemaExtractor(ISchemaExtractor): def __init__( self, - temp_storage: str, + TEMP_DIR: str, ): super().__init__( "Tabular Schema Extractor",