diff --git a/infra/container_app/deploy_container_app_api_web.bicep b/infra/container_app/deploy_container_app_api_web.bicep index fb734e2e..6a377b5f 100644 --- a/infra/container_app/deploy_container_app_api_web.bicep +++ b/infra/container_app/deploy_container_app_api_web.bicep @@ -146,6 +146,10 @@ module containerAppWeb 'deploy_container_app.bicep' = { name: 'APP_MSAL_TOKEN_SCOPE' value: '' } + { + name: 'APP_ISLOGS_ENABLED' + value: 'false' + } ] minReplicas: minReplicaContainerWeb maxReplicas: maxReplicaContainerWeb diff --git a/infra/main.json b/infra/main.json index 98281301..38822f7d 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "14103938519671400586" + "templateHash": "17985360808056860425" } }, "parameters": { @@ -21,7 +21,7 @@ "type": "string", "defaultValue": "EastUs2", "metadata": { - "description": "Location used for Cosmos DB, Container App deployment" + "description": "Location used for Azure Cosmos DB, Azure Container App deployment" } }, "contentUnderstandingLocation": { @@ -35,7 +35,7 @@ "azd": { "type": "location" }, - "description": "Location for the Content Understanding service deployment:" + "description": "Location for the Azure AI Content Understanding service deployment:" }, "minLength": 1 }, @@ -362,7 +362,8 @@ "solutionPrefix": "[format('cps-{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]", "containerImageEndPoint": "cpscontainerreg.azurecr.io", "resourceGroupLocation": "[resourceGroup().location]", - "abbrs": "[variables('$fxv#0')]" + "abbrs": "[variables('$fxv#0')]", + "useLocalBuildLower": "[toLower(parameters('useLocalBuild'))]" }, "resources": [ { @@ -934,7 +935,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "13228064894943437182" + "templateHash": "6622167858340258597" } }, "parameters": { @@ -1432,147 +1433,6 @@ "dependsOn": [ "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiHubName'))]" ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'TENANT-ID')]", - "properties": { - "value": "[subscription().tenantId]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-KEY')]", - "properties": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').key1]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPEN-AI-DEPLOYMENT-MODEL')]", - "properties": { - "value": "[parameters('gptModelName')]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-PREVIEW-API-VERSION')]", - "properties": { - "value": "[parameters('gptModelVersion')]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-ENDPOINT')]", - "properties": { - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-AI-PROJECT-CONN-STRING')]", - "properties": { - "value": "[format('{0};{1};{2};{3}', split(reference(resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiProjectName')), '2024-01-01-preview').discoveryUrl, '/')[2], subscription().subscriptionId, resourceGroup().name, variables('aiProjectName'))]" - }, - "dependsOn": [ - "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiProjectName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-CU-ENDPOINT')]", - "properties": { - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName_cu')), '2021-10-01').endpoint]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName_cu'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-CU-KEY')]", - "properties": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName_cu')), '2021-10-01').key1]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName_cu'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-CU-VERSION')]", - "properties": { - "value": "?api-version=2024-12-01-preview" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-ENDPOINT')]", - "properties": { - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-KEY')]", - "properties": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').key1]" - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-NAME')]", - "properties": { - "value": "[variables('aiServicesName')]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-SUBSCRIPTION-ID')]", - "properties": { - "value": "[subscription().subscriptionId]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-RESOURCE-GROUP')]", - "properties": { - "value": "[resourceGroup().name]" - } - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2021-11-01-preview", - "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-LOCATION')]", - "properties": { - "value": "[parameters('solutionLocation')]" - } } ], "outputs": { @@ -1795,7 +1655,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "15388289119319771934" + "templateHash": "15815884747026956332" } }, "parameters": { @@ -2546,6 +2406,10 @@ { "name": "APP_MSAL_TOKEN_SCOPE", "value": "" + }, + { + "name": "APP_ISLOGS_ENABLED", + "value": "false" } ] }, @@ -3269,7 +3133,7 @@ "location": { "value": "[parameters('secondaryLocation')]" }, - "azureContainerRegistry": "[if(equals(parameters('useLocalBuild'), 'true'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_registry'), '2022-09-01').outputs.acrEndpoint.value), createObject('value', variables('containerImageEndPoint')))]", + "azureContainerRegistry": "[if(equals(variables('useLocalBuildLower'), 'true'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'deploy_container_registry'), '2022-09-01').outputs.acrEndpoint.value), createObject('value', variables('containerImageEndPoint')))]", "appConfigEndPoint": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_app_config_service'), '2022-09-01').outputs.appConfigEndpoint.value]" }, @@ -3304,7 +3168,7 @@ "value": "[parameters('maxReplicaContainerWeb')]" }, "useLocalBuild": { - "value": "[parameters('useLocalBuild')]" + "value": "[variables('useLocalBuildLower')]" } }, "template": { @@ -3314,7 +3178,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "15388289119319771934" + "templateHash": "15815884747026956332" } }, "parameters": { @@ -4065,6 +3929,10 @@ { "name": "APP_MSAL_TOKEN_SCOPE", "value": "" + }, + { + "name": "APP_ISLOGS_ENABLED", + "value": "false" } ] }, diff --git a/src/ContentProcessorAPI/app/libs/cosmos_db/helper.py b/src/ContentProcessorAPI/app/libs/cosmos_db/helper.py index efba5aab..57d6b302 100644 --- a/src/ContentProcessorAPI/app/libs/cosmos_db/helper.py +++ b/src/ContentProcessorAPI/app/libs/cosmos_db/helper.py @@ -102,6 +102,7 @@ def update_document_by_query(self, query: Dict[str, Any], update: Dict[str, Any] result = self.container.update_one(query, {"$set": update}) return result - def delete_document(self, item_id: str): - result = self.container.delete_one({"Id": item_id}) + def delete_document(self, item_id: str, field_name: str = None): + field_name = field_name or "Id" # Use "Id" if field_name is empty or None + result = self.container.delete_one({field_name: item_id}) return result diff --git a/src/ContentProcessorAPI/app/libs/storage_blob/helper.py b/src/ContentProcessorAPI/app/libs/storage_blob/helper.py index a1bf4737..3b76cdd5 100644 --- a/src/ContentProcessorAPI/app/libs/storage_blob/helper.py +++ b/src/ContentProcessorAPI/app/libs/storage_blob/helper.py @@ -90,3 +90,24 @@ def delete_blob_and_cleanup(self, blob_name, container_name=None): # Delete the (virtual) folder in the Container blob_client = container_client.get_blob_client(container_name) blob_client.delete_blob() + + def delete_folder(self, folder_name, container_name=None): + container_client = self._get_container_client(container_name) + + # List all blobs inside the folder + blobs_to_delete = container_client.list_blobs(name_starts_with=folder_name + "/") + + # Delete each blob + for blob in blobs_to_delete: + blob_client = container_client.get_blob_client(blob.name) + blob_client.delete_blob() + + blobs_to_delete = container_client.list_blobs() + if not blobs_to_delete: + + # Get Parent Container + container_client = self._get_container_client() + + # Delete the (virtual) folder in the Container + blob_client = container_client.get_blob_client(folder_name) + blob_client.delete_blob() \ No newline at end of file diff --git a/src/ContentProcessorAPI/app/routers/contentprocessor.py b/src/ContentProcessorAPI/app/routers/contentprocessor.py index bac9c823..e9f197bd 100644 --- a/src/ContentProcessorAPI/app/routers/contentprocessor.py +++ b/src/ContentProcessorAPI/app/routers/contentprocessor.py @@ -6,7 +6,7 @@ import urllib.parse import uuid -from fastapi import APIRouter, Body, Depends, File, UploadFile +from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile from fastapi.responses import JSONResponse, StreamingResponse from pymongo.results import UpdateResult @@ -28,6 +28,7 @@ ContentProcess, ContentProcessorRequest, ContentResultUpdate, + ContentResultDelete, Paging, ProcessFile, Status, @@ -490,3 +491,31 @@ async def get_original_file( return StreamingResponse( file_stream, media_type=content_type_string, headers=headers ) + +@router.delete( + "/processed/{process_id}", + response_model=ContentResultDelete, + summary="Delete the processed content result", + description=""" + Returns the deleted record for a given process ID. + """, +) +async def delete_processed_file( + process_id: str, app_config: AppConfiguration = Depends(get_app_config) + ) -> ContentResultDelete: + try: + deleted_file = CosmosContentProcess(process_id=process_id).delete_processed_file( + connection_string=app_config.app_cosmos_connstr, + database_name=app_config.app_cosmos_database, + collection_name=app_config.app_cosmos_container_process, + storage_connection_string=app_config.app_storage_blob_url, + container_name=app_config.app_cps_processes, + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + return ContentResultDelete( + status="Success" if deleted_file else "Failed", + process_id=deleted_file.process_id if deleted_file else "", + message="" if deleted_file else "This record no longer exists. Please refresh the page." + ) \ No newline at end of file diff --git a/src/ContentProcessorAPI/app/routers/models/contentprocessor/content_process.py b/src/ContentProcessorAPI/app/routers/models/contentprocessor/content_process.py index 4f9b9666..4eb58c12 100644 --- a/src/ContentProcessorAPI/app/routers/models/contentprocessor/content_process.py +++ b/src/ContentProcessorAPI/app/routers/models/contentprocessor/content_process.py @@ -189,6 +189,41 @@ def get_status_from_cosmos( return ContentProcess(**existing_process[0]) else: return None + + def delete_processed_file( + self, + connection_string: str, + database_name: str, + collection_name: str, + storage_connection_string: str, + container_name: str, + ): + """ + Delete the processed file from Cosmos DB & Storage account. + """ + mongo_helper = CosmosMongDBHelper( + connection_string=connection_string, + db_name=database_name, + container_name=collection_name, + indexes=[("process_id", 1)], + ) + + blob_helper = StorageBlobHelper( + account_url=storage_connection_string, container_name=container_name + ) + + # Check if the process_id already exists in the database + existing_process = mongo_helper.find_document( + query={"process_id": self.process_id} + ) + + blob_helper.delete_folder(folder_name=self.process_id) + + if existing_process: + mongo_helper.delete_document(item_id=self.process_id, field_name="process_id") + return ContentProcess(**existing_process[0]) + else: + return None def update_process_result( self, diff --git a/src/ContentProcessorAPI/app/routers/models/contentprocessor/model.py b/src/ContentProcessorAPI/app/routers/models/contentprocessor/model.py index bbe5b124..9cfd45af 100644 --- a/src/ContentProcessorAPI/app/routers/models/contentprocessor/model.py +++ b/src/ContentProcessorAPI/app/routers/models/contentprocessor/model.py @@ -64,6 +64,11 @@ class ContentResultUpdate(BaseModel): process_id: str modified_result: dict +class ContentResultDelete(BaseModel): + process_id: str + status: str + message: str + class ContentCommentUpdate(BaseModel): process_id: str diff --git a/src/ContentProcessorWeb/.dockerignore b/src/ContentProcessorWeb/.dockerignore new file mode 100644 index 00000000..503183c5 --- /dev/null +++ b/src/ContentProcessorWeb/.dockerignore @@ -0,0 +1,8 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + + +**/node_modules \ No newline at end of file diff --git a/src/ContentProcessorWeb/.env b/src/ContentProcessorWeb/.env index 7ef29175..d9bf079d 100644 --- a/src/ContentProcessorWeb/.env +++ b/src/ContentProcessorWeb/.env @@ -9,4 +9,5 @@ REACT_APP_MSAL_POST_REDIRECT_URL = "/" REACT_APP_MSAL_AUTH_SCOPE = APP_MSAL_AUTH_SCOPE -REACT_APP_MSAL_TOKEN_SCOPE = APP_MSAL_TOKEN_SCOPE \ No newline at end of file +REACT_APP_MSAL_TOKEN_SCOPE = APP_MSAL_TOKEN_SCOPE +REACT_APP_ISLOGS_ENABLED = APP_ISLOGS_ENABLED \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/App.tsx b/src/ContentProcessorWeb/src/App.tsx index 03c9b4dd..06462b52 100644 --- a/src/ContentProcessorWeb/src/App.tsx +++ b/src/ContentProcessorWeb/src/App.tsx @@ -2,10 +2,9 @@ import * as React from "react"; import { useEffect } from "react"; import Header from "./Components/Header/Header.tsx"; // Import Header import "./Styles/App.css"; -import "./Components/Panels/Panels.css"; import "./Components/Content/Content.css"; import HomePage from "./Pages/HomePage.tsx"; -import DefaultPage from "./Pages/DefaultPage/DefaultPage.tsx"; +import DefaultPage from "./Pages/DefaultPage"; //import AuxiliaryPage from "./Pages/AuxiliaryPage/AuxiliaryPage.tsx"; import NotFound from "./Pages/NotFound.tsx"; import { ToastContainer } from "react-toastify"; @@ -18,7 +17,7 @@ import { } from "react-router-dom"; import Spinner from "./Components/Spinner/Spinner.tsx"; -import { useDispatch, useSelector,shallowEqual } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { RootState } from './store'; @@ -31,7 +30,7 @@ const App: React.FC = ({ isDarkMode, toggleTheme }) => { const store = useSelector((state: RootState) => ({ loader: state.loader.loadingStack - }),shallowEqual ); + }), shallowEqual); // Apply or remove the "dark-mode" class on the body element based on isDarkMode useEffect(() => { diff --git a/src/ContentProcessorWeb/src/Components/DialogComponent/DialogComponent.tsx b/src/ContentProcessorWeb/src/Components/DialogComponent/DialogComponent.tsx new file mode 100644 index 00000000..be2b439b --- /dev/null +++ b/src/ContentProcessorWeb/src/Components/DialogComponent/DialogComponent.tsx @@ -0,0 +1,64 @@ +import * as React from "react"; +import { + Dialog, + DialogSurface, + DialogTitle, + DialogBody, + DialogContent, + DialogActions, + Button, + useId, +} from "@fluentui/react-components"; + +interface FooterButton { + text: string; + appearance: "primary" | "secondary"; + onClick: () => void; +} + +interface ConfirmationProps { + title: string; + content: string; + isDialogOpen: boolean; // Controlled state for dialog visibility + onDialogClose: () => void; // Function to close the dialog + footerButtons: FooterButton[]; // Array of footer buttons +} + +export const Confirmation: React.FC = ({ + title, + content, + isDialogOpen, + onDialogClose, + footerButtons, +}) => { + const dialogId = useId("dialog-"); + + return ( + + + + {title} + {content} + + {/* Render the footer buttons dynamically */} + {footerButtons.map((button, index) => ( + + ))} + + + + + ); +}; diff --git a/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.styles.scss b/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.styles.scss index 8a89b6ac..f305e1f9 100644 --- a/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.styles.scss +++ b/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.styles.scss @@ -43,5 +43,13 @@ } +.noDataDocContainer{ + display: flex; + justify-content: center; + height: 100%; + border: 1px solid rgb(219, 219, 219); + background: rgb(246, 246, 246); +} + diff --git a/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.tsx b/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.tsx index 40bd4055..9a32ef91 100644 --- a/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.tsx +++ b/src/ContentProcessorWeb/src/Components/DocumentViewer/DocumentViewer.tsx @@ -28,7 +28,7 @@ const DocumentViewer = ({ className, metadata, urlWithSasToken, iframeKey }: IIF const getContentComponent = () => { if (!metadata || !urlWithSasToken) { - return
{t("components.document.none", "No document available")}
; + return

{t("components.document.none", "No document available")}

; } switch (metadata.mimeType) { diff --git a/src/ContentProcessorWeb/src/Components/FluentComponents/Combobox/Combobox.tsx b/src/ContentProcessorWeb/src/Components/FluentComponents/Combobox/Combobox.tsx deleted file mode 100644 index 77ea54bb..00000000 --- a/src/ContentProcessorWeb/src/Components/FluentComponents/Combobox/Combobox.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { - Combobox, - makeStyles, - Option, - useId, -} from "@fluentui/react-components"; -import type { ComboboxProps } from "@fluentui/react-components"; -import './Combobox.styles.scss' -import { useDispatch, useSelector,shallowEqual } from 'react-redux'; -import { RootState } from '../../../store'; -import { setSchemaSelectedOption } from '../../../store/slices/leftPanelSlice'; - - -const useStyles = makeStyles({ - root: { - // Stack the label above the field with a gap - display: "grid", - gridTemplateRows: "repeat(1fr)", - justifyItems: "start", - gap: "2px", - // maxWidth: "250px", - flex: 1 - }, -}); - -interface Option { - key: string; // Assuming `Id` is a string, change to `number` if needed - value: string; -} - -interface SchemaItem { - Id: string; // Adjust type if it's a number - Description: string; -} - -const ComboboxComponent = (props: Partial) => { - const comboId = useId("combo-default"); - const styles = useStyles(); - - const [options, setOptions] = useState([]); - - const [selectedValue, setSelectedValue] = useState([]); - - const dispatch = useDispatch(); - - const store = useSelector((state: RootState) => ({ - schemaData: state.leftPanel.schemaData, - schemaSelectedOption: state.leftPanel.schemaSelectedOption, - schemaLoader: state.leftPanel.schemaLoader, - schemaError: state.leftPanel.schemaError - }),shallowEqual - ); - - React.useEffect(() => { - - // setOptions(store.schemaData.map((item: { ClassName: string; }) => (item as { ClassName: string }).ClassName)); - - setOptions(store.schemaData.map((item: SchemaItem) => { - return { - key: item.Id, - value: item.Description - } - } - )); - - }, [store.schemaData]) - - const handleChange: (typeof props)["onOptionSelect"] = (ev, data) => { - //setSelectedValue(data.selectedOptions); - dispatch(setSchemaSelectedOption(data)) - }; - - return ( -
- - {options.map((option) => ( - - ))} - - {store.schemaError &&
Error: {store.schemaError}
} -
- ); -}; - -export default ComboboxComponent; \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.tsx b/src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.tsx deleted file mode 100644 index b5f45c3f..00000000 --- a/src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { FixedSizeList as List1, ListChildComponentProps } from "react-window"; -import { - FolderRegular, - EditRegular, - OpenRegular, - DocumentRegular, - PeopleRegular, - DocumentPdfRegular, - VideoRegular, - ImageRegular, - EditPersonFilled, -} from "@fluentui/react-icons"; -import { Tooltip } from "@fluentui/react-components"; -import { CaretDown16Filled, CaretUp16Filled, DocumentPdf16Filled ,DocumentQueueAdd20Regular } from "@fluentui/react-icons"; - -import { - PresenceBadgeStatus, - Avatar, - useScrollbarWidth, - useFluent, - TableBody, - TableCell, - TableRow, - Table, - TableHeader, - TableHeaderCell, - TableCellLayout, - TableSelectionCell, - createTableColumn, - useTableFeatures, - useTableSelection, - useTableSort, - TableColumnId, - TableRowData as RowStateBase, - useTableColumnSizing_unstable -} from "@fluentui/react-components"; - -import './GridComponent.styles.scss'; - - -import AutoSizer from "react-virtualized-auto-sizer"; -import { fetchContentTableData /*, fetchTableData*/ } from "../../../Services/apiService"; - - -import { useDispatch, useSelector,shallowEqual } from 'react-redux'; -import { RootState } from '../../../store'; - -import { setSelectedGridRow } from '../../../store/slices/leftPanelSlice'; -import useFileType from "../../../Hooks/useFileType"; - -type Item = { - fileName: { - label: string; - icon: JSX.Element; - }; - imported: { - label: string; - //status: PresenceBadgeStatus; - }; - status: { - label: string; - //timestamp: number; - }; - processTime: { - label: string; - //icon: JSX.Element; - }; - entityScore: { - label: string; - //icon: JSX.Element; - }; - schemaScore: { - label: string; - //icon: JSX.Element; - }; - processId: { - label: string; - //icon: JSX.Element; - }; - lastModifiedBy: { - label: string; - } - file_mime_type: { - label: string; - } -}; - -interface GridComponentProps { - -} - -interface TableRowData extends RowStateBase { - onClick: (e: React.MouseEvent) => void; - onKeyDown: (e: React.KeyboardEvent) => void; - selected: boolean; - appearance: "brand" | "none"; -} - -interface ReactWindowRenderFnProps extends ListChildComponentProps { - data: TableRowData[]; - style: any; - index: any; -} - -const columns = [ - createTableColumn({ - columnId: "fileName", - compare: (a, b) => { - return a.fileName.label.localeCompare(b.fileName.label); - }, - }), - createTableColumn({ - columnId: "imported", - compare: (a, b) => { - const dateA = new Date(a.imported.label).getTime(); - const dateB = new Date(b.imported.label).getTime(); - return dateA - dateB; // Ascending order (oldest to newest) - }, - }), - createTableColumn({ - columnId: "status", - compare: (a, b) => { - return a.status.label.localeCompare(b.status.label); - }, - renderHeaderCell: () => <>
Status
, - }), - createTableColumn({ - columnId: "processTime", - compare: (a, b) => { - return a.processTime.label.localeCompare(b.processTime.label); - }, - }), - createTableColumn({ - columnId: "entityScore", - compare: (a, b) => { - return a.entityScore.label.localeCompare(b.entityScore.label); - }, - }), - createTableColumn({ - columnId: "schemaScore", - compare: (a, b) => { - return a.schemaScore.label.localeCompare(b.schemaScore.label); - }, - }), - createTableColumn({ - columnId: "processId", - compare: (a, b) => { - return a.processId.label.localeCompare(b.processId.label); - }, - }), -]; - - -const GridComponent: React.FC = () => { - - const dispatch = useDispatch(); - - const store = useSelector((state: RootState) => ({ - gridData: state.leftPanel.gridData, - processId: state.leftPanel.processId - }),shallowEqual - ); - - const { targetDocument } = useFluent(); - const scrollbarWidth = useScrollbarWidth({ targetDocument }); - - const [sortState, setSortState] = useState<{ - sortDirection: "ascending" | "descending"; - sortColumn: TableColumnId | undefined; - }>({ - sortDirection: "ascending" as const, - sortColumn: "file", - }); - - const [items, setItems] = useState([]); // State to store fetched items - const [selectedRow, setSelectedRow] = useState(null); // State to store selected row - const { fileType, getMimeType } = useFileType(null); - useEffect(() => { - const getFIleImage = (mimeType:any,file : any)=>{ - if(mimeType ==="application/pdf") return - const mType = getMimeType({name : file}); - switch(mType){ - case 'image/jpeg': - case 'image/png': - case 'image/gif': - case 'image/bmp': - return - - case 'application/pdf': - return - - default : - return - } - } - if (store.gridData.items.length > 0) { - const items = store.gridData.items.map((item: any) => ({ - //fileName: { label: item.processed_file_name, icon: }, - fileName: { - label: item.processed_file_name, - icon: getFIleImage(item.processed_file_mime_type, item.processed_file_name) - }, - imported: { label: item.imported_time }, - status: { label: item.status }, - processTime: { label: item.processed_time ?? "..." }, - entityScore: { label: item.entity_score.toString() }, - schemaScore: { label: item.schema_score.toString() }, - processId: { label: item.process_id }, - lastModifiedBy: { label: item.last_modified_by }, - })); - setItems(items); - //setSelectedRow(items[0].processId.label); - dispatch(setSelectedGridRow({ processId: items[0].processId.label, item: store.gridData.items[0] })) - } - - }, [store.gridData]) - - useEffect(() => { - setSelectedRow(store.processId); - }, [store.processId]) - - const handleRowClick = (processId: string) => { - const selectedItem = store.gridData.items.find((item: any) => item.process_id == processId) - dispatch(setSelectedGridRow({ processId: processId, item: selectedItem })) - } - - const columnSizingOptions = { - fileName: { - idealWidth: 300, - minWidth: 150, - }, - imported: { - minWidth: 110, - defaultWidth: 250, - }, - - }; - - const { - getRows, - sort: { getSortDirection, toggleColumnSort, sort }, - selection: { - allRowsSelected, - someRowsSelected, - toggleAllRows, - toggleRow, - isRowSelected, - }, - } = useTableFeatures( - { - columns, - items, - }, - [ - useTableSelection({ - selectionMode: "multiselect", - defaultSelectedItems: new Set([]), - }), - useTableSort({ - sortState, - onSortChange: (e, nextSortState) => setSortState(nextSortState), - }), - - // useTableColumnSizing_unstable({ - // columnSizingOptions, - // autoFitColumns: false, - // }), - ] - ); - - const renderRoudedButton = (txt: string) => { - return ( - <> -
- - {txt} - -
- - ) - } - - const renderProcessTimeInSeconds = (timeString: string) => { - if (!timeString) { - return
...
; - } - - const parts = timeString.split(":"); - if (parts.length !== 3) { - return
{timeString}
; - } - - const [hours, minutes, seconds] = parts.map(Number); - const totalSeconds = (hours * 3600 + minutes * 60 + seconds).toFixed(2); - - return
{totalSeconds}s
; - }; - - const renderPercentage = (valueText: string, status: string) => { - // Check if the value is a number - const decimalValue = Number(valueText); - if (isNaN(decimalValue) || status !== 'Completed') { - return
...
- }; - - const wholeValue = Math.round(decimalValue * 100); - - let color; - let numberClass = ''; - - // Apply color based on value - if (wholeValue > 80) { - color = '#359B35'; - numberClass = 'gClass' - } else if (wholeValue >= 50 && wholeValue <= 80) { - color = '#C19C00'; - numberClass = 'yClass' - } else if (wholeValue >= 30 && wholeValue < 50) { - color = '#FF5F3DE5'; - numberClass = 'oClass' - } else { - color = '#B10E1C'; - numberClass = 'rClass' - } - - return ( - <> -
- {wholeValue}% - {(wholeValue > 50) ? - - : - - } -
- - ) - } - - const calculateSchemaScore = (valueText: string, lastModifiedBy: string, status: string) => { - if (lastModifiedBy === 'user') { - return ( -
- - Verified - -
- ); - } - return renderPercentage(valueText, status); - } - - const rendertext = (text: any, type = '') => { - if (type === 'date') { - const date = new Date(text); - const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getDate().toString().padStart(2, "0")}/${date.getFullYear()}`; - return
{formattedDate}
; - } - return ( - <>
{text}
- ) - } - - const rows: TableRowData[] = sort(getRows((row) => { - const selected = isRowSelected(row.rowId); - return { - ...row, - onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === " ") { - e.preventDefault(); - toggleRow(e, row.rowId); - } - }, - selected, - appearance: selected ? ("brand" as const) : ("none" as const), - }; - })); - - const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { - const { item, selected, appearance, onClick, onKeyDown } = data[index]; - const isSelected = item.processId.label === selectedRow; - return ( - handleRowClick(item.processId.label)} - //onClick={onClick} - //appearance={appearance} - appearance={isSelected ? "brand" : "none"} // Change appearance based on selection - className={isSelected ? "selectedRow" : ""} - > - {/* */} - - - - {item.fileName.label} - - - - - {rendertext(item.imported.label, 'date')} - - - {renderRoudedButton(item.status.label)} - - - {renderProcessTimeInSeconds(item.processTime.label)} - - - - {renderPercentage(item.entityScore.label, item.status.label)} - - - - {calculateSchemaScore(item.schemaScore.label, item.lastModifiedBy.label, item.status.label)} - - - - ); - }; - - const toggleAllKeydown = React.useCallback( - (e: React.KeyboardEvent) => { - if (e.key === " ") { - toggleAllRows(e); - e.preventDefault(); - } - }, - [toggleAllRows] - ); - - const headerSortProps = (columnId: TableColumnId) => ({ - onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId), - sortDirection: getSortDirection(columnId), - }); - - return ( -
- - - - {/* */} - File name - Imported - Status - Process time - Entity score - Schema score - {/** Scrollbar alignment for the header */} -
- - - -
- - {({ height, width }) => ( - - {RenderRow} - - )} - -
-
-
-
- ); -}; - -export default GridComponent; \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Components/JSONEditor/JSONEditor.tsx b/src/ContentProcessorWeb/src/Components/JSONEditor/JSONEditor.tsx index 9f93f5d9..2760e7f4 100644 --- a/src/ContentProcessorWeb/src/Components/JSONEditor/JSONEditor.tsx +++ b/src/ContentProcessorWeb/src/Components/JSONEditor/JSONEditor.tsx @@ -24,14 +24,18 @@ const JSONEditor: React.FC = () => { useEffect(() => { - if (Object.keys(store.contentData).length > 0) { - const formattedJson = store.contentData.result; - const data = { - extracted_result: { - ...formattedJson + if(!store.cLoader){ + if (Object.keys(store.contentData).length > 0) { + const formattedJson = store.contentData.result; + const data = { + extracted_result: { + ...formattedJson + } } + setJsonData(data); + }else { + setJsonData({}) } - setJsonData(data); } }, [store.contentData]) @@ -42,7 +46,8 @@ const JSONEditor: React.FC = () => { return ( <>{ - store.cLoader ?

Loading...

: + store.cLoader ?

Loading...

: + Object.keys(jsonData).length == 0 ?

No data available

: { - return ( -
- {/* PanelToolbar */} - -
- ); -}; - -// No need to change the export, even if copied. -export default PanelLeft; diff --git a/src/ContentProcessorWeb/src/Components/Panels/PanelRight.tsx b/src/ContentProcessorWeb/src/Components/Panels/PanelRight.tsx deleted file mode 100644 index 11ec8e5a..00000000 --- a/src/ContentProcessorWeb/src/Components/Panels/PanelRight.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import { Button } from "@fluentui/react-components"; -import { MoreHorizontalRegular, Sparkle20Filled } from "@fluentui/react-icons"; -import PanelToolbar from "../../Hooks/usePanelHooks.tsx"; - -// Visit https://mochimilk.github.io/cto_coral_docs/index.html#/developers/panels for documentation. - -// It is recommended that you create copies of PanelLeft.tsx and/or PanelRight.tsx and move them into dedicated folders under Pages. -// See file structure in src/Pages/DefaultPage and the imports section in DefaultPage.tsx. - -const PanelRight: React.FC = () => { - return ( -
- {/* PanelHeader */} - } - header="Copilot" - > -
- ); -}; - -// No need to change the export, even if copied. -export default PanelRight; diff --git a/src/ContentProcessorWeb/src/Components/Panels/Panels.css b/src/ContentProcessorWeb/src/Components/Panels/Panels.css deleted file mode 100644 index 5bf4adca..00000000 --- a/src/ContentProcessorWeb/src/Components/Panels/Panels.css +++ /dev/null @@ -1,170 +0,0 @@ -/* Panel container styling */ - -.panelLeft { - /* background-color: var(--colorNeutralBackground4); */ - position: relative; - display: flex; - height: 100%; - box-sizing: border-box; - flex-direction: column; - overflow-y: auto; - background-color: white; - border: 1px solid #D6D6D6; - border-width: 1px 1px 0px 0px; -} - -.panelRight { - /* background-color: var(--colorNeutralBackground4); */ - position: relative; - display: flex; - height: 100%; - box-sizing: border-box; - flex-direction: column; - overflow-y: auto; - background-color: white; - border: 1px solid #D6D6D6; - border-width: 1px 0px 0px 0px; -} - -/* Resize functionality on panels */ - -.resize-handle-left, -.resize-handle-right { - position: absolute; - top: 0; - width: 3px; - height: 100%; - cursor: ew-resize; - background-color: transparent; -} - -.resize-handle-left { - right: 0; -} - -.resize-handle-right { - left: 0; - z-index: 1; -} - -.resize-handle-left:hover, -.resize-handle-right:hover { - background-color: var(--colorNeutralStroke1); -} - -.resize-handle-right:hover { - background-color: var(--colorNeutralStroke1); -} - -.resize-handle-left:active, -.resize-handle-right:active { - background-color: var(--colorNeutralStroke1); -} - -.contentContainer { - display: flex; - flex: 1; - flex-direction: column; - width: 100%; - height: 100%; - align-items: start; - box-sizing: border-box; -} - -.content { - display: flex; - flex-direction: column; - flex: 1 1 auto; - overflow: scroll; - box-sizing: border-box; - padding: 64px 16px 64px 16px; - height: 100%; - width: 100%; - margin: auto; - overflow-x: hidden; - overflow-y: auto; - margin-top: -64px; - -} - -.panelToolbar { - display: flex; - width: 100%; - align-items: center; - padding: 16px 16px; - min-height: 64px; - justify-content: space-between; - box-sizing: border-box; - backdrop-filter: saturate(180%) blur(16px); - transition: all 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); -} - -.buttonGroup { - display: flex; - align-items: center; -} - -.headerTitleGroup { - display: flex; - align-items: center; - gap: 8px; - width: 100%; -} - -.leftcontent { - display: flex; - flex-direction: column; - flex: 1 1 auto; - overflow: scroll; - box-sizing: border-box; - padding: 0px 16px 16px 16px; - height: 100%; - width: 100%; - margin: auto; - overflow-x: hidden; - overflow-y: auto; - -} - -.panelRightContent { - display: flex; - flex-direction: column; - flex: 1 1 auto; - overflow: scroll; - box-sizing: border-box; - padding: 0px 16px 16px 16px; - height: 100%; - width: 100%; - margin: auto; - overflow-x: hidden; - overflow-y: auto; - /* margin-top: -64px; */ -} - -.fullHeight { - height: 100vh; -} - -.brand, -.selectedRow { - background-color: #e0e0e0; - /* Light gray for selected row */ - border: 2px solid #0078d4; - /* Blue border for selected row */ -} - -/* .JSONEditorClass { - max-height: none !important; -} */ - -.right-loader{ - display: flex; - justify-content: center; - align-items: center; - height: 100%; -} - -.custom-test button{ - padding : 0px !important; - padding :5px 12px 12px 10px !important -} \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Components/UploadContent/uploadContent.tsx b/src/ContentProcessorWeb/src/Components/UploadContent/uploadContent.tsx deleted file mode 100644 index e3792561..00000000 --- a/src/ContentProcessorWeb/src/Components/UploadContent/uploadContent.tsx +++ /dev/null @@ -1,121 +0,0 @@ -// import * as React from "react"; -// import { Button } from "@fluentui/react-components"; -// import { Dialog, DialogSurface, DialogBody, DialogTitle, DialogActions, DialogTrigger } from "@fluentui/react-components"; -// import { Table, TableBody, TableCell, TableHeader, TableRow, TableHeaderCell } from "@fluentui/react-components"; -// import { ProgressBar } from "@fluentui/react-components"; - -// interface FileUploadProps { -// isOpen: boolean; -// onClose: () => void; -// } - -// const API_BASE_URL = process.env.REACT_APP_API_BASE_URL; - -// const FileUpload: React.FC = ({ isOpen, onClose }) => { -// const [selectedFiles, setSelectedFiles] = React.useState([]); -// const [uploading, setUploading] = React.useState(false); -// const [progress, setProgress] = React.useState(0); - -// const handleFileChange = (event: React.ChangeEvent) => { -// if (event.target.files) { -// setSelectedFiles(Array.from(event.target.files)); -// } -// }; - -// const handleUpload = async () => { -// if (selectedFiles.length === 0) { -// alert("Please select files first."); -// return; -// } - -// setUploading(true); -// setProgress(0); - -// // const formData = new FormData(); -// // selectedFiles.forEach((file) => { -// // formData.append("file", file); -// // formData.append("metadata_id", crypto.randomUUID()); -// // formData.append("schema_id", "schema_id"); -// // }); - - -// // update -// const metadata = { -// Metadata_Id: crypto.randomUUID(), -// Schema_Id: "Schema 001", -// }; - -// const formData = new FormData(); - -// selectedFiles.forEach((file) => { -// // Attach the file -// formData.append("file", file); -// // Attach JSON metadata (replace the name if needed by your API) -// formData.append("data", JSON.stringify(metadata)); -// }); - - -// try { -// const response = await fetch(`${API_BASE_URL}/upload`, { -// method: "POST", -// body: formData, -// }); - -// if (response.ok) { -// const responseData = await response.json(); -// alert("Files uploaded successfully!"); -// setSelectedFiles([]); -// } else { -// alert("Upload failed. Try again."); -// } -// } catch (error) { -// console.error("Upload error:", error); -// alert("Error uploading files."); -// } finally { -// setUploading(false); -// onClose(); // Close the dialog after upload -// } -// }; - -// return ( -// !data.open && onClose()}> -// -// -// Upload Documents -// -// - -// {selectedFiles.length > 0 && ( -// -// -// -// File Name -// Size (bytes) -// -// -// -// {selectedFiles.map((file) => ( -// -// {file.name} -// {file.size} -// -// ))} -// -//
-// )} - -// {uploading && } - -// -// -// -// -//
-//
-//
-// ); -// }; - -// export default FileUpload; \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Hooks/useApi.ts b/src/ContentProcessorWeb/src/Hooks/useApi.ts deleted file mode 100644 index aafb53c8..00000000 --- a/src/ContentProcessorWeb/src/Hooks/useApi.ts +++ /dev/null @@ -1,126 +0,0 @@ -// src/hooks/useApi.ts -import { useState, useEffect } from 'react'; -import useAuth from '../msal-auth/useAuth'; // Importing your custom useAuth hook - -const api = process.env.REACT_APP_API_BASE_URL; // base API URL - -interface RequestOptions { - method: string; - headers: { [key: string]: string }; - body?: string | FormData | null; -} - -interface ApiResponse { - [key: string]: any; -} - -// Custom hook to fetch API with authentication -const useApi = () => { - const { getToken } = useAuth(); // Call useAuth hook to get the token - const [token, setToken] = useState(null); - - // Fetch token on mount - useEffect(() => { - const fetchToken = async () => { - const fetchedToken = await getToken(); // Fetch the token asynchronously - //setToken(fetchedToken || null); // Save the token to state - }; - - fetchToken(); - }, [getToken]); - - // Function to make API requests with JWT token - const fetchWithAuth = async ( - url: string, - method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', - body: any = null - ): Promise => { - if (!token) { - throw new Error("Token is not available"); - } - - const headers: { [key: string]: string } = { - 'Authorization': `Bearer ${token}`, // Add token to Authorization header - 'Accept': 'application/json', // Add Accept header - 'Content-Type': 'application/json', // Default to application/json - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': '*' - }; - - if (body instanceof FormData) { - delete headers['Content-Type']; // Don't set Content-Type if the body is FormData - } - - const options: RequestOptions = { - method, - headers, - }; - - if (body) { - options.body = JSON.stringify(body); // Add body for POST/PUT - } - - try { - const response = await fetch(`${api}${url}`, options); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Something went wrong'); - } - - const isJson = response.headers.get('content-type')?.includes('application/json'); - return isJson ? await response.json() : null; - } catch (error) { - console.error('API Error:', (error as Error).message); - throw error; - } - }; - - // Function to make API requests without authentication (for login) - const fetchWithoutAuth = async ( - url: string, - method: 'POST' = 'POST', - body: any = null - ): Promise => { - const headers: { [key: string]: string } = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }; - - const options: RequestOptions = { - method, - headers, - }; - - if (body) { - options.body = JSON.stringify(body); // Add the body for POST - } - - try { - const response = await fetch(`${api}${url}`, options); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Login failed'); - } - - const isJson = response.headers.get('content-type')?.includes('application/json'); - return isJson ? await response.json() : null; - } catch (error) { - console.error('Login Error:', (error as Error).message); - throw error; - } - }; - - return { - get: (url: string): Promise => fetchWithAuth(url, 'GET'), - post: (url: string, body: any): Promise => fetchWithAuth(url, 'POST', body), - put: (url: string, body: any): Promise => fetchWithAuth(url, 'PUT', body), - delete: (url: string): Promise => fetchWithAuth(url, 'DELETE'), - upload: (url: string, formData: FormData): Promise => fetchWithAuth(url, 'POST', formData), - login: (url: string, body: any): Promise => fetchWithoutAuth(url, 'POST', body), // For login without auth - }; -}; - -export default useApi; diff --git a/src/ContentProcessorWeb/src/Hooks/useConsoleSuppression.ts b/src/ContentProcessorWeb/src/Hooks/useConsoleSuppression.ts index c9fa7079..2c724db7 100644 --- a/src/ContentProcessorWeb/src/Hooks/useConsoleSuppression.ts +++ b/src/ContentProcessorWeb/src/Hooks/useConsoleSuppression.ts @@ -3,7 +3,7 @@ import { useEffect } from "react"; // Custom hook to suppress console logs, warnings, and errors in localhost const useConsoleSuppression = () => { useEffect(() => { - if (window.location.hostname !== "localhost") { + if (window.location.hostname !== "localhost" && process.env.REACT_APP_ISLOGS_ENABLED?.toLocaleLowerCase() != "true" ) { // Save the original console methods const originalConsoleError = console.error; const originalConsoleWarn = console.warn; diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/CustomCellRender.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/CustomCellRender.tsx new file mode 100644 index 00000000..794d8c3e --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/CustomCellRender.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { CaretUp16Filled, CaretDown16Filled, EditPersonFilled } from '@fluentui/react-icons'; +import { Button, Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-components'; +import { MoreVerticalRegular, MoreVerticalFilled, bundleIcon } from '@fluentui/react-icons'; + +type CellRendererProps = { + type: string; + props?: any; +}; + +const MoreVerticallIcon = bundleIcon( + MoreVerticalFilled, + MoreVerticalRegular +); + +const CellRenderer: React.FC = ({ type, props }) => { + // Destructure props based on type + const { + txt, timeString, valueText, status, lastModifiedBy, text, item, deleteBtnStatus, setSelectedDeleteItem, toggleDialog, + } = props || {}; + + // Render for rounded button + const renderRoundedButton = (txt: string) => ( +
+ {txt} +
+ ); + + // Render for processing time + const renderProcessTimeInSeconds = (timeString: string) => { + if (!timeString) { + return
...
; + } + + const parts = timeString.split(":"); + if (parts.length !== 3) { + return
{timeString}
; + } + + const [hours, minutes, seconds] = parts.map(Number); + const totalSeconds = (hours * 3600 + minutes * 60 + seconds).toFixed(2); + + return
{totalSeconds}s
; + }; + + // Render for percentage + const renderPercentage = (valueText: string, status: string) => { + const decimalValue = Number(valueText); + if (isNaN(decimalValue) || status !== 'Completed') { + return
...
; + } + + const wholeValue = Math.round(decimalValue * 100); + let color; + let numberClass = ''; + + // Apply color based on value + if (wholeValue > 80) { + color = '#359B35'; + numberClass = 'gClass'; + } else if (wholeValue >= 50 && wholeValue <= 80) { + color = '#C19C00'; + numberClass = 'yClass'; + } else if (wholeValue >= 30 && wholeValue < 50) { + color = '#FF5F3DE5'; + numberClass = 'oClass'; + } else { + color = '#B10E1C'; + numberClass = 'rClass'; + } + + return ( +
+ {wholeValue}% + {wholeValue > 50 ? ( + + ) : ( + + )} +
+ ); + }; + + // Render for schema score + const calculateSchemaScore = (valueText: string, lastModifiedBy: string, status: string) => { + if (lastModifiedBy === 'user') { + return ( +
+ + Verified + +
+ ); + } + return renderPercentage(valueText, status); + }; + + // Render for text + const renderText = (text: any, type = '') => { + if (type === 'date') { + const date = new Date(text); + const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getDate().toString().padStart(2, "0")}/${date.getFullYear()}`; + return
{formattedDate}
; + } + return
{text}
; + }; + + // Render for delete button + const renderDeleteButton = (item: any, deleteBtnStatus: any) => ( + + + + ); + + // Switch based on type + switch (type) { + case 'roundedButton': + return renderRoundedButton(txt || ''); + case 'processTime': + return renderProcessTimeInSeconds(timeString || ''); + case 'percentage': + return renderPercentage(valueText || '', status || ''); + case 'schemaScore': + return calculateSchemaScore(valueText || '', lastModifiedBy || '', status || ''); + case 'text': + return renderText(text, 'center'); + case 'date': + return renderText(text, 'date'); + case 'deleteButton': + return renderDeleteButton(item, deleteBtnStatus || {}); + default: + return
Invalid Type
; + } +}; + +export default CellRenderer; diff --git a/src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.styles.scss b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.styles.scss similarity index 73% rename from src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.styles.scss rename to src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.styles.scss index 74c29338..d965cad7 100644 --- a/src/ContentProcessorWeb/src/Components/FluentComponents/GridComponent/GridComponent.styles.scss +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.styles.scss @@ -1,30 +1,36 @@ -.col0 { - min-width: 3% !important; -} - .col1 { - min-width: 20% !important; + min-width: 120px !important; } .col2 { - min-width: 10% !important; + min-width: 55px !important; + max-width: 85px !important; } .col3 { - min-width: 12% !important; + min-width: 45px !important; + max-width: 70px !important; } .col4 { - min-width: 12% !important; + min-width: 35px !important; + max-width: 85px !important; } .col5 { - min-width: 12% !important; + min-width: 35px !important; + max-width: 85px !important; } .col6 { - min-width: 12% !important; + min-width: 45px !important; + max-width: 85px !important; +} +.col7 { + min-width: 30px !important; + max-width: 35px !important; + box-sizing: border-box; } .roundedBtn { @@ -38,6 +44,7 @@ white-space: nowrap; font-size: 10px; text-transform: capitalize; + span { color: #2C3C85 } @@ -78,7 +85,7 @@ // max-height: calc(100vh - 235px) !important; // height: calc(100vh - 235px) !important; flex: 1 1 auto; - height : 100%; + height: 100%; //background-color: red; >div { @@ -95,12 +102,13 @@ .centerAlign { text-align: center; } + .percentageContainer { display: flex; align-items: center; justify-content: center; - } - +} + .editPersonIcon { margin-right: 4px; display: flex; diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx new file mode 100644 index 00000000..2cf0c51a --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGrid.tsx @@ -0,0 +1,368 @@ +import React, { useState, useEffect } from "react"; +import { FixedSizeList as List, ListChildComponentProps } from "react-window"; +import { DocumentQueueAdd20Regular, DocumentPdfRegular, ImageRegular } from "@fluentui/react-icons"; +import { TableCellActions, Tooltip } from "@fluentui/react-components"; +import { + PresenceBadgeStatus, Avatar, useScrollbarWidth, useFluent, TableBody, TableCell, TableRow, Table, + TableHeader, TableHeaderCell, TableCellLayout, TableSelectionCell, createTableColumn, useTableFeatures, + useTableSelection, useTableSort, TableColumnId, useTableColumnSizing_unstable, + TableRowId +} from "@fluentui/react-components"; +import './ProcessQueueGrid.styles.scss'; +import AutoSizer from "react-virtualized-auto-sizer"; +import CustomCellRender from "./CustomCellRender"; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { RootState, AppDispatch } from '../../../../store'; +import { setSelectedGridRow, deleteProcessedFile, fetchContentTableData } from '../../../../store/slices/leftPanelSlice'; +import useFileType from "../../../../Hooks/useFileType"; +import { Confirmation } from "../../../../Components/DialogComponent/DialogComponent.tsx"; +import { Item, TableRowData, ReactWindowRenderFnProps, GridComponentProps } from './ProcessQueueGridTypes.ts'; + +const columns = [ + createTableColumn({ + columnId: "fileName", + compare: (a, b) => { + return a.fileName.label.localeCompare(b.fileName.label); + }, + }), + createTableColumn({ + columnId: "imported", + compare: (a, b) => { + const dateA = new Date(a.imported.label).getTime(); + const dateB = new Date(b.imported.label).getTime(); + return dateA - dateB; // Ascending order (oldest to newest) + }, + }), + createTableColumn({ + columnId: "status", + compare: (a, b) => { + return a.status.label.localeCompare(b.status.label); + }, + renderHeaderCell: () => <>
Status
, + }), + createTableColumn({ + columnId: "processTime", + compare: (a, b) => { + return a.processTime.label.localeCompare(b.processTime.label); + }, + }), + createTableColumn({ + columnId: "entityScore", + compare: (a, b) => { + return a.entityScore.label.localeCompare(b.entityScore.label); + }, + }), + createTableColumn({ + columnId: "schemaScore", + compare: (a, b) => { + return a.schemaScore.label.localeCompare(b.schemaScore.label); + }, + }), + createTableColumn({ + columnId: "processId", + compare: (a, b) => { + return a.processId.label.localeCompare(b.processId.label); + }, + }), +]; + + +const ProcessQueueGrid: React.FC = () => { + + const dispatch = useDispatch(); + const store = useSelector((state: RootState) => ({ + gridData: state.leftPanel.gridData, + processId: state.leftPanel.processId, + deleteFilesLoader: state.leftPanel.deleteFilesLoader, + pageSize: state.leftPanel.pageSize, + gridLoader: state.leftPanel.gridLoader + }), shallowEqual + ); + + const { targetDocument } = useFluent(); + const scrollbarWidth = useScrollbarWidth({ targetDocument }); + + const [sortState, setSortState] = useState<{ + sortDirection: "ascending" | "descending"; + sortColumn: TableColumnId | undefined; + }>({ + sortDirection: "ascending" as const, + sortColumn: "file", + }); + + const [items, setItems] = useState([]); // State to store fetched items + const { fileType, getMimeType } = useFileType(null); + + const [selectedRows, setSelectedRows] = React.useState( + () => new Set([0]) + ); + + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const [selectedDeleteItem, setSelectedDeleteItem] = useState(null); + + useEffect(() => { + const getFIleImage = (mimeType: any, file: any) => { + if (mimeType === "application/pdf") return + const mType = getMimeType({ name: file }); + switch (mType) { + case 'image/jpeg': + case 'image/png': + case 'image/gif': + case 'image/bmp': + return + case 'application/pdf': + return + default: + return + } + } + if (!store.gridLoader) { + if (store.gridData.items.length > 0) { + const items = store.gridData.items.map((item: any) => ({ + fileName: { + label: item.processed_file_name, + icon: getFIleImage(item.processed_file_mime_type, item.processed_file_name) + }, + imported: { label: item.imported_time }, + status: { label: item.status }, + processTime: { label: item.processed_time ?? "..." }, + entityScore: { label: item.entity_score.toString() }, + schemaScore: { label: item.schema_score.toString() }, + processId: { label: item.process_id }, + lastModifiedBy: { label: item.last_modified_by }, + })); + setItems(items); + } else { + setItems([]) + } + } + + }, [store.gridData]) + + useEffect(() => { + console.log("selectedRows", selectedRows, items); + if (items.length > 0 && selectedRows.size > 0) { + const selectedRow = [...selectedRows][0]; + if (typeof selectedRow === 'number') { + let selectedItem = items[selectedRow]; + if (!selectedItem) { + setSelectedRows(new Set([0])); + } else { + console.log("selectedItem", selectedItem); + const findItem = getSelectedItem(selectedItem?.processId.label ?? ''); + dispatch(setSelectedGridRow({ processId: selectedItem?.processId.label, item: findItem })); + } + + } else { + console.error("Selected row is not a valid index", selectedRow); + } + } + }, [selectedRows, items]) + + const getSelectedItem = (processId: string) => { + const findItem = store.gridData.items.find((item: any) => item.process_id == processId) + return findItem; + } + + const { + getRows, + sort: { getSortDirection, toggleColumnSort, sort }, + selection: { + allRowsSelected, + someRowsSelected, + toggleAllRows, + toggleRow, + isRowSelected, + }, + } = useTableFeatures( + { + columns, + items, + }, + [ + useTableSelection({ + selectionMode: "single", + selectedItems: selectedRows, + onSelectionChange: (e, data) => { + setSelectedRows(data.selectedItems) + }, + }), + useTableSort({ + sortState, + onSortChange: (e, nextSortState) => setSortState(nextSortState), + }), + ] + ); + + + + const rows: TableRowData[] = sort(getRows((row) => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => { + if (!e.defaultPrevented) { + toggleRow(e, row.rowId); + } + }, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === " " && !e.defaultPrevented) { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? "brand" : "none", + }; + })); + + const onClickCellActions = (e: React.MouseEvent) => + e.preventDefault(); + const onKeyDownCellActions = (e: React.KeyboardEvent) => + e.key === " " && e.preventDefault(); + + const isDeleteDisabled = (processId: string, status: string) => { + if (status != 'Completed' && status != 'Error') return { disabled: true, message: 'Inprogress' } + if (store.deleteFilesLoader.includes(processId)) return { disabled: true, message: 'Deleting' } + return { disabled: false, message: '' } + }; + + const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { + const { item, selected, appearance, onClick, onKeyDown } = data[index]; + const deleteBtnStatus = isDeleteDisabled(item.processId.label, item.status.label); + return ( + + + + + {item.fileName.label} + + + + + + + + + + + + + + + + + + + + + + + + ); + }; + + const headerSortProps = (columnId: TableColumnId) => ({ + onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId), + sortDirection: getSortDirection(columnId), + }); + + const handleDelete = async () => { + if (selectedDeleteItem) { + try { + toggleDialog(); + await dispatch(deleteProcessedFile({ processId: selectedDeleteItem.processId.label ?? null })); + } catch (error: any) { + + } finally { + dispatch(fetchContentTableData({ pageSize: store.pageSize, pageNumber: 1 })).unwrap(); + } + } + }; + + const toggleDialog = () => { + setIsDialogOpen(!isDialogOpen); + }; + + return ( + <> +
+ + + + File name + Imported + Status + Process time + Entity score + Schema score + + {/*
*/} + + + +
+ + {({ height, width }) => ( + + {RenderRow} + + )} + +
+
+
+
+ + + + ); +}; + +export default ProcessQueueGrid; \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGridTypes.ts b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGridTypes.ts new file mode 100644 index 00000000..2cdc6eaf --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/ProcessQueueGrid/ProcessQueueGridTypes.ts @@ -0,0 +1,29 @@ +import { TableRowId, TableRowData as RowStateBase, } from "@fluentui/react-components"; +import { ListChildComponentProps } from "react-window"; + +export interface Item { + fileName: { label: string; icon: JSX.Element }; + imported: { label: string }; + status: { label: string }; + processTime: { label: string }; + entityScore: { label: string }; + schemaScore: { label: string }; + processId: { label: string }; + lastModifiedBy: { label: string }; + file_mime_type: { label: string }; +} + +export interface TableRowData extends RowStateBase { + onClick: (e: React.MouseEvent) => void; + onKeyDown: (e: React.KeyboardEvent) => void; + selected: boolean; + appearance: "brand" | "none"; +} + +export interface ReactWindowRenderFnProps extends ListChildComponentProps { + data: TableRowData[]; + style: any; + index: number; +} + +export interface GridComponentProps { } diff --git a/src/ContentProcessorWeb/src/Components/FluentComponents/Combobox/Combobox.styles.scss b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdown.styles.scss similarity index 100% rename from src/ContentProcessorWeb/src/Components/FluentComponents/Combobox/Combobox.styles.scss rename to src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdown.styles.scss diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdown.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdown.tsx new file mode 100644 index 00000000..68e15226 --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdown.tsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from "react"; +import { Combobox, makeStyles, Option, useId } from "@fluentui/react-components"; +import type { ComboboxProps } from "@fluentui/react-components"; +import './SchemaDropdown.styles.scss'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { RootState } from '../../../../store'; +import { setSchemaSelectedOption } from '../../../../store/slices/leftPanelSlice'; +import { OptionList, SchemaItem, StoreState } from './SchemaDropdownTypes'; // Importing types + +const useStyles = makeStyles({ + root: { + display: "grid", + gridTemplateRows: "repeat(1fr)", + justifyItems: "start", + gap: "2px", + flex: 1 + }, +}); + +const ComboboxComponent = (props: Partial) => { + const comboId = useId("combo-default"); + const styles = useStyles(); + + const [options, setOptions] = useState([]); + + const dispatch = useDispatch(); + + const store = useSelector( + (state: RootState) => ({ + schemaData: state.leftPanel.schemaData, + schemaSelectedOption: state.leftPanel.schemaSelectedOption, + schemaError: state.leftPanel.schemaError, + }), + shallowEqual + ); + + useEffect(() => { + setOptions( + store.schemaData.map((item: SchemaItem) => ({ + key: item.Id, + value: item.Description, + })) + ); + }, [store.schemaData]); + + const handleChange: (typeof props)["onOptionSelect"] = (ev, data) => { + dispatch(setSchemaSelectedOption(data)); + }; + + return ( +
+ + {options.map((option) => ( + + ))} + + {store.schemaError &&
Error: {store.schemaError}
} +
+ ); +}; + +export default ComboboxComponent; diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdownTypes.ts b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdownTypes.ts new file mode 100644 index 00000000..ba4424b9 --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Components/SchemaDropdown/SchemaDropdownTypes.ts @@ -0,0 +1,17 @@ +// types.ts +export interface OptionList { + key: string; // Assuming `Id` is a string, change to `number` if needed + value: string; +} + +export interface SchemaItem { + Id: string; // Adjust type if it's a number + Description: string; +} + +export interface StoreState { + schemaData: SchemaItem[]; + schemaSelectedOption: { optionText: string } | null; + schemaLoader: boolean; + schemaError: string | null; +} diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPageContent.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx similarity index 98% rename from src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPageContent.tsx rename to src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx index c93c9626..5cd861d1 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPageContent.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelCenter.tsx @@ -126,7 +126,7 @@ const ContentDevelopers: React.FC = ({ isSavingInProgress: state.centerPanel.isSavingInProgress, processStepsData: state.centerPanel.processStepsData, selectedItem : state.leftPanel.selectedItem, - activeProcessId : state.centerPanel.activeProcessId + activeProcessId : state.centerPanel.activeProcessId, }), shallowEqual ); @@ -184,7 +184,7 @@ const ContentDevelopers: React.FC = ({ /> ) :

No data available

} - ), [store.activeProcessId,store.selectedItem]); + ), [store.activeProcessId,store.selectedItem,store.contentData]); const ProcessHistory = useCallback(() => (
diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelLeft.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelLeft.tsx similarity index 79% rename from src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelLeft.tsx rename to src/ContentProcessorWeb/src/Pages/DefaultPage/PanelLeft.tsx index 570ea66f..695ed077 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelLeft.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelLeft.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect } from "react"; import { Button } from "@fluentui/react-components"; import { ArrowClockwiseRegular, ArrowUploadRegular } from "@fluentui/react-icons"; import PanelToolbar from "../../Hooks/usePanelHooks.tsx"; -import GridComponent from '../../Components/FluentComponents/GridComponent/GridComponent.tsx'; -import ComboboxComponent from '../../Components/FluentComponents/Combobox/Combobox'; -import UploadFilesModal from "../../Components/UploadContent/UploadFilesModal"; +import ProcessQueueGrid from './Components/ProcessQueueGrid/ProcessQueueGrid.tsx'; +import SchemaDropdown from './Components/SchemaDropdown/SchemaDropdown'; +import UploadFilesModal from "../../Components/UploadContent/UploadFilesModal.tsx"; -import { useDispatch, useSelector,shallowEqual } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { fetchSchemaData, fetchContentTableData } from '../../store/slices/leftPanelSlice.ts'; -import { AppDispatch,RootState } from '../../store'; -import { startLoader, stopLoader } from "../../store/slices/loaderSlice"; +import { AppDispatch, RootState } from '../../store/index.ts'; +import { startLoader, stopLoader } from "../../store/slices/loaderSlice.ts"; import { toast } from "react-toastify"; interface PanelLeftProps { @@ -22,9 +22,9 @@ const PanelLeft: React.FC = () => { const store = useSelector((state: RootState) => ({ schemaSelectedOption: state.leftPanel.schemaSelectedOption, - page_size : state.leftPanel.gridData.page_size, + page_size: state.leftPanel.gridData.page_size, pageSize: state.leftPanel.pageSize - }),shallowEqual ); + }), shallowEqual); useEffect(() => { const fetchData = async () => { @@ -51,7 +51,7 @@ const PanelLeft: React.FC = () => { } catch (error) { console.error("Error fetching data:", error); } finally { - dispatch(stopLoader("1")); + dispatch(stopLoader("1")); } } @@ -60,12 +60,13 @@ const PanelLeft: React.FC = () => {
- +
- +
); diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelRight.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelRight.tsx similarity index 97% rename from src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelRight.tsx rename to src/ContentProcessorWeb/src/Pages/DefaultPage/PanelRight.tsx index 29046f7c..798bcbcb 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPagePanelRight.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/PanelRight.tsx @@ -43,6 +43,8 @@ const PanelRight: React.FC = () => { const isExists = isBlobExists(); if(store.fileResponse.length > 0 && isExists && isExists.processId == store.processId){ setFileData({ 'urlWithSasToken': isExists.blobURL, 'mimeType': isExists.headers['content-type'] }) + }else { + setFileData({ 'urlWithSasToken': '', 'mimeType': '' }) } },[store.processId, store.fileResponse]) diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/Panels.styles.scss b/src/ContentProcessorWeb/src/Pages/DefaultPage/Panels.styles.scss new file mode 100644 index 00000000..94c29e6b --- /dev/null +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/Panels.styles.scss @@ -0,0 +1,170 @@ +/* Panel container styling */ + +.panelLeft { + /* background-color: var(--colorNeutralBackground4); */ + position: relative; + display: flex; + height: 100%; + box-sizing: border-box; + flex-direction: column; + overflow-y: auto; + background-color: white; + border: 1px solid #D6D6D6; + border-width: 1px 1px 0px 0px; + } + + .panelRight { + /* background-color: var(--colorNeutralBackground4); */ + position: relative; + display: flex; + height: 100%; + box-sizing: border-box; + flex-direction: column; + overflow-y: auto; + background-color: white; + border: 1px solid #D6D6D6; + border-width: 1px 0px 0px 0px; + } + + /* Resize functionality on panels */ + + .resize-handle-left, + .resize-handle-right { + position: absolute; + top: 0; + width: 3px; + height: 100%; + cursor: ew-resize; + background-color: transparent; + } + + .resize-handle-left { + right: 0; + } + + .resize-handle-right { + left: 0; + z-index: 1; + } + + .resize-handle-left:hover, + .resize-handle-right:hover { + background-color: var(--colorNeutralStroke1); + } + + .resize-handle-right:hover { + background-color: var(--colorNeutralStroke1); + } + + .resize-handle-left:active, + .resize-handle-right:active { + background-color: var(--colorNeutralStroke1); + } + + .panelCenter { + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + height: 100%; + align-items: start; + box-sizing: border-box; + } + + .content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + overflow: scroll; + box-sizing: border-box; + padding: 64px 16px 64px 16px; + height: 100%; + width: 100%; + margin: auto; + overflow-x: hidden; + overflow-y: auto; + margin-top: -64px; + + } + + .panelToolbar { + display: flex; + width: 100%; + align-items: center; + padding: 16px 16px; + min-height: 64px; + justify-content: space-between; + box-sizing: border-box; + backdrop-filter: saturate(180%) blur(16px); + transition: all 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); + } + + .buttonGroup { + display: flex; + align-items: center; + } + + .headerTitleGroup { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + } + + .leftcontent { + display: flex; + flex-direction: column; + flex: 1 1 auto; + overflow: scroll; + box-sizing: border-box; + padding: 0px 16px 16px 16px; + height: 100%; + width: 100%; + margin: auto; + overflow-x: hidden; + overflow-y: auto; + + } + + .panelRightContent { + display: flex; + flex-direction: column; + flex: 1 1 auto; + overflow: scroll; + box-sizing: border-box; + padding: 0px 16px 16px 16px; + height: 100%; + width: 100%; + margin: auto; + overflow-x: hidden; + overflow-y: auto; + /* margin-top: -64px; */ + } + + .fullHeight { + height: 100vh; + } + + .brand, + .selectedRow { + background-color: #e0e0e0; + /* Light gray for selected row */ + border: 2px solid #0078d4; + /* Blue border for selected row */ + } + + /* .JSONEditorClass { + max-height: none !important; + } */ + + .right-loader{ + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + + .custom-test button{ + padding : 0px !important; + padding :5px 12px 12px 10px !important + } \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPage.tsx b/src/ContentProcessorWeb/src/Pages/DefaultPage/index.tsx similarity index 74% rename from src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPage.tsx rename to src/ContentProcessorWeb/src/Pages/DefaultPage/index.tsx index 4a1a01b8..1b70f737 100644 --- a/src/ContentProcessorWeb/src/Pages/DefaultPage/DefaultPage.tsx +++ b/src/ContentProcessorWeb/src/Pages/DefaultPage/index.tsx @@ -1,9 +1,10 @@ import * as React from "react"; -import ContentDevelopers from "./DefaultPageContent.tsx"; -import PanelLeft from "./DefaultPagePanelLeft.tsx"; -import PanelRight from "./DefaultPagePanelRight.tsx"; +import PanelCenter from "./PanelCenter.tsx"; +import PanelLeft from "./PanelLeft.tsx"; +import PanelRight from "./PanelRight.tsx"; import { makeStyles } from "@fluentui/react-components"; +import './Panels.styles.scss'; // AppHooks import { useAppHooks } from "../../Hooks/useAppHooks.tsx"; @@ -16,12 +17,11 @@ const Page: React.FC = () => { {isPanelOpen && (
-
)} -
- + { - const apiUrl = process.env.REACT_APP_API_BASE_URL; - const response = await fetch(`${apiUrl}/schemavault/`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - // Add other headers if necessary - }, - }); - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - const responseData = await response.json(); - - //console.log("response ", response.json()); - return responseData; - }; - - -{ /* Fetch data for Grid Component (Contents Table) */ } -export const fetchContentTableData = async (pageSize = 50): Promise => { - try { - const apiUrl = process.env.REACT_APP_API_BASE_URL; - //console.log("apiUrl fetchContent", apiUrl) - let pageNumber = 1 - let allItems: any[] = []; - let totalPages = 0; - console.log(JSON.stringify({ - page_size: pageSize, - page_number: pageNumber, - })); - - do { - const response = await fetch(`${apiUrl}/contentprocessor/processed`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cache-Control': 'no-cache', - }, - body: JSON.stringify({ - "page_size" : pageSize, - "page_number" : pageNumber - - - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - - const responseData = await response.json(); - totalPages = responseData.total_pages; - //console.log(responseData); - - // Transform each item from the API into the format expected by the table - const transformedItems = responseData.items.map((item: any) => ({ - fileName: { label: item.processed_file_name, icon: "📄" }, - imported: { label: new Date(item.imported_time).toLocaleString() }, - status: { label: item.status }, - processTime: { label: item.processed_time }, - entityScore: { label: item.entity_score.toString() }, - schemaScore: { label: item.schema_score.toString() }, - processId: { label: item.process_id }, - lastModifiedBy: { label: item.last_modified_by }, - })); - - allItems = allItems.concat(transformedItems); - pageNumber++; - } while (pageNumber - <= totalPages); - - return { - data: allItems, - currentPage: 1, - totalPages: totalPages, - pageSize: pageSize, - totalItems: allItems.length, - }; - } catch (error) { - console.error("Error fetching content table data:", error); - throw error; - } -}; - -export const uploadFile = async (file: File, schema: string) => { - //const schemaId = store.dispatch.; - const metadata = { - Metadata_Id: crypto.randomUUID(), - Schema_Id: schema, - }; - - const formData = new FormData(); - - // Attach the file - formData.append("file", file); - - // Attach JSON metadata (replace the name if needed by your API) - formData.append("data", JSON.stringify(metadata)); - - try { - const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/contentprocessor/submit`, { - method: "POST", - body: formData, - }); - - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - - const responseData = await response.json(); - //console.log(responseData); - - return responseData; - } catch (error) { - console.error("Error uploading file:", error); - throw error; - } -} - -export const fetchContentJson = async (processId: string) => { - - try { - const resposne = await fetch(`${process.env.REACT_APP_API_BASE_URL}/contentprocessor/processed/${processId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!resposne.ok) { - throw new Error(`Error: ${resposne.status} ${resposne.statusText}`); - } - - const responseData = await resposne.json(); - - //return responseData; - - return { - extractedResults: responseData.result, - comment: responseData.comment - }; - } catch (error) { - console.error("Error fetching JSON data:", error); - throw error; - } -}; - -export const fetchContentFileData = async (processId: string) => { - - try { - //console.log("document api", `${process.env.REACT_APP_API_BASE_URL}/contentprocessor/processed/files/${processId}`); - const resposne = await fetch(`${process.env.REACT_APP_API_BASE_URL}/contentprocessor/processed/files/${processId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!resposne.ok) { - throw new Error(`Error: ${resposne.status} ${resposne.statusText}`); - } - - const responseData = await resposne.blob(); - //console.log("docuemnt blob data", responseData); - - return responseData; - - // return { - // extractedResults: responseData.result, - // comment: responseData.comment - // }; - } catch (error) { - console.error("Error fetching document data:", error); - throw error; - } -} - - -export const saveContentJson = async (processId: string, contentJson: string) => { - try { - const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/contentprocessor/processed/${processId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - }, - body: JSON.stringify({ - "process_id": processId, - "modified_result": contentJson, - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - - const responseData = await response.json(); - //console.log(responseData); - - return responseData; - - } catch (error) { - console.error("Error saving JSON data:", error); - throw error; - } -} - -export const saveContentComment = async (processId: string, comment: string) => { - try { - const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/contentprocessor/processed/${processId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - }, - body: JSON.stringify({ - "process_id": processId, - "comment": comment, - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.status} ${response.statusText}`); - } - - const responseData = await response.json(); - //console.log(responseData); - - return responseData; - - } catch (error) { - console.error("Error saving comment data:", error); - throw error; - } -} \ No newline at end of file diff --git a/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts b/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts index 6bd44b63..a3d9f795 100644 --- a/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts +++ b/src/ContentProcessorWeb/src/store/slices/centerPanelSlice.ts @@ -115,6 +115,8 @@ const centerPanelSlice = createSlice({ .addCase(fetchContentJsonData.rejected, (state, action) => { state.cError = action.error.message || 'An error occurred'; state.cLoader = false; + state.contentData = []; + state.comments = ""; //console.error("Error fetching JSON data:", action.error.message || 'An error occurred'); }); @@ -127,8 +129,9 @@ const centerPanelSlice = createSlice({ toast.success("Data saved successfully!"); // Success toast state.isSavingInProgress = false; }) - .addCase(saveContentJson.rejected, (state, action) => { - toast.error("Date saving failed !"); + .addCase(saveContentJson.rejected, (state, action : any) => { + console.log("action", action) + toast.error(JSON.parse(action.error.message).message); state.isSavingInProgress = false; }); diff --git a/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts b/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts index e38b5264..a0fedd44 100644 --- a/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts +++ b/src/ContentProcessorWeb/src/store/slices/leftPanelSlice.ts @@ -10,9 +10,11 @@ interface LeftPanelState { schemaLoader: boolean; schemaSelectedOption: any; gridData: any; + gridLoader: boolean; processId: string | null; selectedItem: any; pageSize : number; + deleteFilesLoader: string[] } interface UploadMetadata { @@ -43,6 +45,21 @@ export const fetchContentTableData = createAsyncThunk('/contentprocessor/deleteProcessedFile/', async ({ processId }, {rejectWithValue}) => { + if (!processId) { + return rejectWithValue("Reset store"); + } + const url = '/contentprocessor/processed/' + processId; + const response = await httpUtility.delete(url); + console.log("response", response); + return response as DeleteApiResponse; ; +}); + export const uploadFile = createAsyncThunk< any, // Type for fulfilled response { file: File; schema: string } // Type for the input payload @@ -89,9 +106,13 @@ const initialState: LeftPanelState = { schemaError: null, gridData: {...gridDefaultVal}, + gridLoader : false, processId: null, selectedItem: {}, pageSize : 500, + + deleteFilesLoader : [], + }; const leftPanelSlice = createSlice({ @@ -126,16 +147,17 @@ const leftPanelSlice = createSlice({ builder .addCase(fetchContentTableData.pending, (state) => { //state.schemaError = null; + state.gridLoader = true; state.gridData = {...gridDefaultVal}; }) .addCase(fetchContentTableData.fulfilled, (state, action: PayloadAction) => { // Adjust `any` to the response data type //state.schemaLoader = false; - state.gridData = action.payload + state.gridData = action.payload; + state.gridLoader = false; }) .addCase(fetchContentTableData.rejected, (state, action) => { - // state.schemaError = action.error.message || 'An error occurred'; - //state.schemaLoader = false; - //console.error("Error fetching content table data : ", action.error.message || 'An error occurred'); + state.gridLoader = false; + toast.error('Something went wrong!') }); @@ -155,6 +177,31 @@ const leftPanelSlice = createSlice({ }); + //Fetch Grid Data + builder + .addCase(deleteProcessedFile.pending, (state, action) => { + const processId = action.meta.arg.processId; + if (processId) { + state.deleteFilesLoader.push(processId); + } + }) + .addCase(deleteProcessedFile.fulfilled, (state, action) => { + const processId = action.meta.arg.processId; + if (processId) { + state.deleteFilesLoader = state.deleteFilesLoader.filter(id => id !== processId); + } + if(action.payload.status === 'Success') + toast.success("File deleted successfully.") + else + toast.error(action.payload.message) + }) + .addCase(deleteProcessedFile.rejected, (state, action) => { + const processId = action.meta.arg.processId; + if (processId) { + state.deleteFilesLoader = state.deleteFilesLoader.filter(id => id !== processId); + toast.error("Failed to delete the file. Please try again.") + } + }); }, });