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

Ensaremirerol/issue11 #27

Merged
merged 12 commits into from
Feb 7, 2025
12 changes: 9 additions & 3 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ single executable using Nuitka.

- Latest Java Runtime Environment (JRE) or Java Development Kit (JDK) to use the
[RMLMapper](https://github.com/RMLio/rmlmapper-java)

- This is required to generate RML mappings.

- For MacOS, you need system version 15.0(Sequoia) or later to run the
executable.

## Installation 🚀

From [Releases](https://github.com/MaastrichtU-IDS/RDFCraft/releases) page,
Expand Down Expand Up @@ -61,10 +65,12 @@ You can either use Codespaces or a local devcontainer to start developing.

For Codespaces, just open the repository in Codespaces and you are good to go.

For local devcontainer, you need to have Docker installed on your machine. Then clone the repository and run the following command and open the repository in VSCode. It should prompt you to open the repository in a devcontainer.

Or you can use Command Palette (Ctrl/Command+Shift+P) and search for `Dev Container: Reopen in Container`.
For local devcontainer, you need to have Docker installed on your machine. Then
clone the repository and run the following command and open the repository in
VSCode. It should prompt you to open the repository in a devcontainer.

Or you can use Command Palette (Ctrl/Command+Shift+P) and search for
`Dev Container: Reopen in Container`.

### Manual Installation

Expand Down
25 changes: 25 additions & 0 deletions app/src/lib/api/mapping_service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ class MappingService {
`Failed to update mapping: ${result.message} (status: ${result.status})`,
);
}

public static async importMapping(
data: File,
workspaceUuid: string,
): Promise<boolean> {
const formData = new FormData();
formData.append('tar', data);
const result = await this.getApiClient().callApi<boolean>(
`/workspaces/${workspaceUuid}/mapping/import`,
{
method: 'POST',
body: formData,
parser: () => true,
timeout: 0,
},
);

if (result.type === 'success') {
return result.data;
}

throw new Error(
`Failed to import mapping: ${result.message} (status: ${result.status})`,
);
}
}

export default MappingService;
49 changes: 49 additions & 0 deletions app/src/lib/api/workspaces_api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,55 @@ class WorkspacesApi {
`Failed to delete workspace: ${result.message} (status: ${result.status})`,
);
}

public static async exportWorkspace(uuid: string): Promise<{
blob: Blob;
fileName: string;
}> {
const result = await this.getApiClient().callApi<{
blob: Blob;
fileName: string
}>(
`/workspaces/${uuid}/export`,
{
method: 'GET',
parser: (data) => data as { blob: Blob; fileName: string },
},
);

if (result.type === 'success') {
return result.data;
}

throw new Error(
`Failed to export workspace: ${result.message} (status: ${result.status})`,
);
}

public static async importWorkspace(
file: File,
): Promise<boolean> {
const formData = new FormData();
formData.append('tar', file);

const result = await this.getApiClient().callApi<boolean>(
'/workspaces/import',
{
method: 'POST',
body: formData,
parser: () => true,
timeout: 0,
},
);

if (result.type === 'success') {
return result.data;
}

throw new Error(
`Failed to import workspace: ${result.message} (status: ${result.status})`,
);
}
}

export default WorkspacesApi;
1 change: 1 addition & 0 deletions app/src/lib/api/yarrrml_service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class YARRRMLService {
method: 'POST',
body: rml,
parser: data => data as string,
timeout: 0,
},
);

Expand Down
4 changes: 4 additions & 0 deletions app/src/lib/services/api_service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class ApiService {
return ApiService.instances[baseUrl];
}

get baseUrl(): string {
return this._baseUrl;
}

async callApi<T, D = unknown>(
endpoint: string,
options: ApiCallOptions<T, D>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ const CreateMappingDialog = (props: CreateMappingDialogProps) => {
setError('Please fill all fields');
return;
}
// json_path must end with '[*]'
if (sourceType === 'json' && json_path.value.slice(-3) !== '[*]') {
setError('JSON Path must end with "[*]"');
return;
}

const extra = sourceType === 'json' ? { json_path: json_path.value } : {};

Expand Down
5 changes: 5 additions & 0 deletions app/src/pages/workspace_page/components/MappingCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ interface MappingCardItemProps {
mapping: MappingGraph;
onSelected: (mapping: MappingGraph) => void;
onDelete: (mapping: MappingGraph) => void;
onExport: (mapping: MappingGraph) => void;
}

const MappingCardItem = ({
mapping,
onSelected,
onDelete,
onExport,
}: MappingCardItemProps) => {
return (
<CardItem
Expand All @@ -26,6 +28,9 @@ const MappingCardItem = ({
<Button intent='danger' onClick={() => onDelete(mapping)}>
Delete
</Button>
<Button intent='primary' onClick={() => onExport(mapping)}>
Export
</Button>
<Button intent='primary' onClick={() => onSelected(mapping)}>
Open
</Button>
Expand Down
23 changes: 23 additions & 0 deletions app/src/pages/workspace_page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const WorkspacePage = () => {
const loadWorkspace = useWorkspacePageState(state => state.loadWorkspace);
const createMapping = useWorkspacePageState(state => state.createMapping);
const deleteMapping = useWorkspacePageState(state => state.deleteMapping);
const exportMapping = useWorkspacePageState(state => state.exportMapping);
const importMapping = useWorkspacePageState(state => state.importMapping);

useEffect(() => {
if (props.uuid) {
Expand All @@ -43,6 +45,21 @@ const WorkspacePage = () => {
setOpen(null);
}, [toBeDeleted, workspace, deleteMapping]);

const handleImport = useCallback(() => {
if (workspace) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.tar.gz';
fileInput.onchange = async () => {
if (fileInput.files) {
const file = fileInput.files[0];
importMapping(workspace.uuid, file);
}
};
fileInput.click();
}
}, [workspace, importMapping]);

return (
<div className='workspace-page'>
<DeleteAlert
Expand Down Expand Up @@ -93,6 +110,9 @@ const WorkspacePage = () => {
>
Create New Mapping
</Button>
<Button icon='import' onClick={handleImport}>
Import Mapping
</Button>
<Button
icon='search-around'
onClick={() => {
Expand Down Expand Up @@ -147,6 +167,9 @@ const WorkspacePage = () => {
`/workspaces/${workspace.uuid}/mapping/${mappingGraph.uuid}`,
);
}}
onExport={mappingGraph => {
exportMapping(workspace.uuid, mappingGraph.uuid);
}}
/>
))}
</div>
Expand Down
25 changes: 25 additions & 0 deletions app/src/pages/workspace_page/state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Workspace } from '../../lib/api/workspaces_api/types';

import ApiService from '@/lib/services/api_service';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import MappingService from '../../lib/api/mapping_service';
Expand All @@ -25,6 +26,8 @@ interface WorkspacePageStateActions {
extra: Record<string, unknown>,
) => Promise<void>;
deleteMapping: (workspaceUuid: string, mappingUuid: string) => Promise<void>;
exportMapping: (workspaceUuid: string, mappingUuid: string) => Promise<void>;
importMapping: (workspaceUuid: string, data: File) => Promise<void>;
}

const defaultState: WorkspacePageState = {
Expand Down Expand Up @@ -103,6 +106,28 @@ const functions: ZustandActions<
set({ isLoading: null });
});
},
async exportMapping(workspaceUuid, mappingUuid) {
set({ isLoading: 'Exporting mapping...' });
const a = document.createElement('a');
a.href = `${ApiService.getInstance('default').baseUrl}workspaces/${workspaceUuid}/mapping/${mappingUuid}/export`;
a.click();
set({ isLoading: null });
},
async importMapping(workspaceUuid, data) {
set({ isLoading: 'Importing mapping...' });
MappingService.importMapping(data, workspaceUuid)
.then(() => {
return get().loadWorkspace(workspaceUuid);
})
.catch(error => {
if (error instanceof Error) {
set({ error: error.message });
}
})
.finally(() => {
set({ isLoading: null });
});
},
});

const useWorkspacePageState = create<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import {
FormGroup,
InputGroup,
} from '@blueprintjs/core';
import { ItemRenderer, Select } from '@blueprintjs/select';
import { useRef, useState } from 'react';
import BasicSelectMenuItem, {
BasicSelectItem,
} from '../../../../components/BasicSelectMenuItem';
import { BasicSelectItem } from '../../../../components/BasicSelectMenuItem';
import { CreateWorkspaceMetadata } from '../../../../lib/api/workspaces_api/types';

import './styles.scss';
Expand All @@ -22,18 +19,18 @@ interface CreateWorkspaceDialogProps {
onConfirm: (data: CreateWorkspaceMetadata) => void;
}

const WORKSPACE_TYPES: BasicSelectItem[] = [
{
value: 'local',
text: 'Local',
label: '',
},
{
value: 'remote',
text: 'Remote',
label: '',
},
];
// const WORKSPACE_TYPES: BasicSelectItem[] = [
// {
// value: 'local',
// text: 'Local',
// label: '',
// },
// {
// value: 'remote',
// text: 'Remote',
// label: '',
// },
// ];

const CreateWorkspaceDialog = ({
open,
Expand All @@ -48,18 +45,18 @@ const CreateWorkspaceDialog = ({

const [error, setError] = useState<string | null>(null);

const renderItem: ItemRenderer<BasicSelectItem> = (
item,
{ handleClick, modifiers, query },
) => {
return (
<BasicSelectMenuItem
key={item.value}
item={item}
itemRendererProps={{ handleClick, modifiers, query }}
/>
);
};
// const renderItem: ItemRenderer<BasicSelectItem> = (
// item,
// { handleClick, modifiers, query },
// ) => {
// return (
// <BasicSelectMenuItem
// key={item.value}
// item={item}
// itemRendererProps={{ handleClick, modifiers, query }}
// />
// );
// };

const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand All @@ -73,10 +70,10 @@ const CreateWorkspaceDialog = ({
setError('Name is required');
return;
}
if (!type.value) {
setError('Type is required');
return;
}
// if (!type.value) {
// setError('Type is required');
// return;
// }
if (type.value === 'remote' && !remote_connection_string.value) {
setError('Remote Connection String is required');
return;
Expand All @@ -85,7 +82,7 @@ const CreateWorkspaceDialog = ({
const data: CreateWorkspaceMetadata = {
name: workspace_name.value,
description: description.value || '',
type: type.value,
type: 'local',
location: remote_connection_string?.value || '',
};
onConfirm(data);
Expand Down Expand Up @@ -120,7 +117,7 @@ const CreateWorkspaceDialog = ({
<FormGroup label='Description' labelFor='description'>
<InputGroup id='description' name='description' />
</FormGroup>
<FormGroup
{/* <FormGroup
label='Type'
labelFor='type'
labelInfo='(required)'
Expand Down Expand Up @@ -151,7 +148,7 @@ const CreateWorkspaceDialog = ({
rightIcon='double-caret-vertical'
/>
</Select>
</FormGroup>
</FormGroup> */}
{workspaceType?.value === 'remote' && (
<FormGroup
label='Remote Connection String'
Expand Down
Loading