diff --git a/databox/api/src/Api/Model/Input/AssetInput.php b/databox/api/src/Api/Model/Input/AssetInput.php index b0fa668f7..c71e93e4b 100644 --- a/databox/api/src/Api/Model/Input/AssetInput.php +++ b/databox/api/src/Api/Model/Input/AssetInput.php @@ -30,6 +30,7 @@ class AssetInput extends AbstractOwnerIdInput /** * @var Workspace */ + #[Assert\NotNull] public $workspace; public ?Collection $collection = null; diff --git a/databox/api/src/Api/Model/Output/ESDocumentOutput.php b/databox/api/src/Api/Model/Output/ESDocumentOutput.php new file mode 100644 index 000000000..076c6498b --- /dev/null +++ b/databox/api/src/Api/Model/Output/ESDocumentOutput.php @@ -0,0 +1,32 @@ +data; + } +} diff --git a/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php b/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php new file mode 100644 index 000000000..a1d54f37b --- /dev/null +++ b/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php @@ -0,0 +1,45 @@ +itemProvider->provide($operation, $uriVariables, $context); + $this->denyAccessUnlessGranted(AbstractVoter::READ, $asset); + $this->denyAccessUnlessGranted(JwtUser::ROLE_TECH); + + if ($asset instanceof Asset) { + $indexName = $this->elasticSearchClient->getIndexName('asset'); + $response = $this->elasticSearchClient->request($indexName.'/_doc/'.$asset->getId(), [], Request::GET); + + return new ESDocumentOutput($response->getData()); + } + + return null; + } +} diff --git a/databox/api/src/Elasticsearch/ElasticSearchClient.php b/databox/api/src/Elasticsearch/ElasticSearchClient.php index e58c76df7..3c1099fc0 100644 --- a/databox/api/src/Elasticsearch/ElasticSearchClient.php +++ b/databox/api/src/Elasticsearch/ElasticSearchClient.php @@ -12,6 +12,7 @@ public function __construct( private Client $client, private Index $assetIndex, + private Index $collectionIndex, ) { } diff --git a/databox/api/src/Entity/Core/Asset.php b/databox/api/src/Entity/Core/Asset.php index a0b6314af..05c7c40b9 100644 --- a/databox/api/src/Entity/Core/Asset.php +++ b/databox/api/src/Entity/Core/Asset.php @@ -24,6 +24,7 @@ use App\Api\Model\Input\MultipleAssetInput; use App\Api\Model\Input\PrepareDeleteAssetsInput; use App\Api\Model\Output\AssetOutput; +use App\Api\Model\Output\ESDocumentOutput; use App\Api\Model\Output\MultipleAssetOutput; use App\Api\Model\Output\PrepareDeleteAssetsOutput; use App\Api\Processor\AssetAttributeBatchUpdateProcessor; @@ -35,6 +36,7 @@ use App\Api\Processor\RemoveAssetFromCollectionProcessor; use App\Api\Processor\TriggerAssetWorkflowProcessor; use App\Api\Provider\AssetCollectionProvider; +use App\Api\Provider\AssetElasticsearchDocumentProvider; use App\Api\Provider\SearchSuggestionCollectionProvider; use App\Controller\Core\DeleteAssetByIdsAction; use App\Controller\Core\DeleteAssetByKeysAction; @@ -66,7 +68,8 @@ new Get( normalizationContext: [ 'groups' => [self::GROUP_READ, Collection::GROUP_ABSOLUTE_TITLE], - ] + ], + security: 'is_granted("'.AbstractVoter::READ.'", object)', ), new Delete( uriTemplate: '/assets/{id}/collections/{collectionId}', @@ -102,7 +105,10 @@ processor: PrepareSubstitutionProcessor::class, ), new GetCollection(), - new Post(securityPostDenormalize: 'is_granted("CREATE", object)'), + new Post( + securityPostDenormalize: 'is_granted("CREATE", object)', + validate: true, + ), new Post( uriTemplate: '/assets/multiple', normalizationContext: [ @@ -153,6 +159,12 @@ controller: DeleteAssetByIdsAction::class, name: 'delete_by_ids', ), + new Get( + uriTemplate: '/assets/{id}/es-document', + output: ESDocumentOutput::class, + name: 'es_document', + provider: AssetElasticsearchDocumentProvider::class, + ), ], normalizationContext: [ 'groups' => [self::GROUP_LIST], diff --git a/databox/client/src/api/asset.ts b/databox/client/src/api/asset.ts index 276e52510..8cd05aa4d 100644 --- a/databox/client/src/api/asset.ts +++ b/databox/client/src/api/asset.ts @@ -1,10 +1,6 @@ import apiClient from './api-client'; import {Asset, AssetFileVersion, Attribute, Collection, Share} from '../types'; -import { - ApiCollectionResponse, - getAssetsHydraCollection, - getHydraCollection, -} from './hydra'; +import {ApiCollectionResponse, getAssetsHydraCollection, getHydraCollection,} from './hydra'; import {AxiosRequestConfig} from 'axios'; import {TFacets} from '../components/Media/Asset/Facets'; @@ -96,9 +92,11 @@ export async function getSearchSuggestions( } export async function getAsset(id: string): Promise { - const res = await apiClient.get(`/assets/${id}`); + return (await apiClient.get(`/assets/${id}`)).data; +} - return res.data; +export async function getAssetESDocument(id: string): Promise { + return (await apiClient.get(`/assets/${id}/es-document`)).data.data; } export async function getAssetShares(assetId: string): Promise { diff --git a/databox/client/src/components/Dialog/Asset/AssetDialog.tsx b/databox/client/src/components/Dialog/Asset/AssetDialog.tsx index 6561c4d71..8bf60f916 100644 --- a/databox/client/src/components/Dialog/Asset/AssetDialog.tsx +++ b/databox/client/src/components/Dialog/Asset/AssetDialog.tsx @@ -15,6 +15,8 @@ import OperationsAsset from './OperationsAsset'; import {modalRoutes} from '../../../routes'; import {useNavigateToModal} from '../../Routing/ModalLink.tsx'; import AssetWorkflow from './AssetWorkflow.tsx'; +import {useAuth} from '@alchemy/react-auth'; +import AssetESDocument from "./AssetESDocument.tsx"; type Props = {}; @@ -22,6 +24,7 @@ export default function AssetDialog({}: Props) { const {t} = useTranslation(); const {id} = useParams(); const navigateToModal = useNavigateToModal(); + const {user} = useAuth(); const [data, setData] = useState(); @@ -128,6 +131,15 @@ export default function AssetDialog({}: Props) { }, enabled: data.capabilities.canEdit, }, + { + title: t('asset.manage.es_doc.title', 'ES Document'), + component: AssetESDocument, + id: 'es_doc', + props: { + data, + }, + enabled: user?.roles?.includes('tech') ?? false, + }, ]} /> ); diff --git a/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx b/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx new file mode 100644 index 000000000..42172d0c4 --- /dev/null +++ b/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx @@ -0,0 +1,50 @@ +import {Asset, StateSetter} from '../../../types'; +import {DialogTabProps} from '../Tabbed/TabbedDialog'; +import ContentTab from '../Tabbed/ContentTab'; +import {getAssetESDocument} from '../../../api/asset'; +import {useCallback, useEffect, useState} from "react"; +import RefreshIcon from '@mui/icons-material/Refresh'; +import {IconButton, LinearProgress, Typography} from "@mui/material"; + +type Props = { + data: Asset; + setData: StateSetter; +} & DialogTabProps; + +export default function AssetESDocument({ + data, + onClose, + minHeight, +}: Props) { + const [document, setDocument] = useState(); + const [loading, setLoading] = useState(false); + + const refresh = useCallback(async () => { + setLoading(true); + try { + setDocument(await getAssetESDocument(data.id)); + } finally { + setLoading(false); + } + }, [data.id]); + + useEffect(() => { + refresh(); + }, [refresh]); + + return ( + + {loading && } + {document ? <> + + + +
+                    {JSON.stringify(document, null, 4)}
+                
+ : null} +
+ ); +}