diff --git a/databox/api/src/Api/Model/Output/ESDocumentOutput.php b/databox/api/src/Api/Model/Output/ESDocumentOutput.php deleted file mode 100644 index 076c6498b..000000000 --- a/databox/api/src/Api/Model/Output/ESDocumentOutput.php +++ /dev/null @@ -1,32 +0,0 @@ -data; - } -} diff --git a/databox/api/src/Api/Model/Output/ESDocumentStateOutput.php b/databox/api/src/Api/Model/Output/ESDocumentStateOutput.php new file mode 100644 index 000000000..6c087d14f --- /dev/null +++ b/databox/api/src/Api/Model/Output/ESDocumentStateOutput.php @@ -0,0 +1,29 @@ +data; + } + + public function getSynced(): bool + { + return $this->synced; + } +} diff --git a/databox/api/src/Api/Processor/AssetElasticsearchDocumentSyncProcessor.php b/databox/api/src/Api/Processor/AssetElasticsearchDocumentSyncProcessor.php new file mode 100644 index 000000000..d897ef1e9 --- /dev/null +++ b/databox/api/src/Api/Processor/AssetElasticsearchDocumentSyncProcessor.php @@ -0,0 +1,34 @@ +denyAccessUnlessGranted(JwtUser::ROLE_TECH); + $this->deferredIndexListener->scheduleForUpdate($data); + + return new Response('', 201); + } +} diff --git a/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php b/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php index a1d54f37b..0a636d0b1 100644 --- a/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php +++ b/databox/api/src/Api/Provider/AssetElasticsearchDocumentProvider.php @@ -8,13 +8,11 @@ use Alchemy\AuthBundle\Security\Traits\SecurityAwareTrait; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; -use App\Api\Model\Output\ESDocumentOutput; use App\Api\Traits\ItemProviderAwareTrait; -use App\Elasticsearch\ElasticSearchClient; +use App\Elasticsearch\ESDocumentStateManager; use App\Entity\Core\Asset; use App\Security\Voter\AbstractVoter; use Doctrine\ORM\EntityManagerInterface; -use Elastica\Request; final class AssetElasticsearchDocumentProvider implements ProviderInterface { @@ -23,7 +21,7 @@ final class AssetElasticsearchDocumentProvider implements ProviderInterface public function __construct( private readonly EntityManagerInterface $em, - private readonly ElasticSearchClient $elasticSearchClient, + private readonly ESDocumentStateManager $esDocumentStateManager, ) { } @@ -34,10 +32,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $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 $this->esDocumentStateManager->getObjectState($asset); } return null; diff --git a/databox/api/src/Elasticsearch/ESDocumentStateManager.php b/databox/api/src/Elasticsearch/ESDocumentStateManager.php new file mode 100644 index 000000000..fc4e69acd --- /dev/null +++ b/databox/api/src/Elasticsearch/ESDocumentStateManager.php @@ -0,0 +1,48 @@ +getObjectPersister($object)->transformToElasticaDocument($object); + $indexName = $this->elasticSearchClient->getIndexName($document->getIndex()); + $response = $this->elasticSearchClient->request($indexName.'/_doc/'.$object->getId(), [], Request::GET); + + $data = $response->getData(); + $synced = $document->getData() == $data['_source']; + + return new ESDocumentStateOutput($data, $synced); + } + + private function getObjectPersister(object $object): ObjectPersister + { + foreach ($this->objectPersisters as $objectPersister) { + if ($objectPersister instanceof ObjectPersister && $objectPersister->handlesObject($object)) { + return $objectPersister; + } + } + + throw new RuntimeException(sprintf('No object persister found for object of class %s', get_class($object))); + } +} diff --git a/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php b/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php index e9af882ed..af2e9a17c 100644 --- a/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php +++ b/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php @@ -105,7 +105,6 @@ private function compileAttributes(Asset $asset): array $data[$locale][$fieldName][] = $translation; } } - } else { foreach ($v as $locale => $translation) { $data[$locale][$fieldName] = $translation; diff --git a/databox/api/src/Entity/Core/Asset.php b/databox/api/src/Entity/Core/Asset.php index 05c7c40b9..1da4a441f 100644 --- a/databox/api/src/Entity/Core/Asset.php +++ b/databox/api/src/Entity/Core/Asset.php @@ -24,10 +24,11 @@ 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\ESDocumentStateOutput; use App\Api\Model\Output\MultipleAssetOutput; use App\Api\Model\Output\PrepareDeleteAssetsOutput; use App\Api\Processor\AssetAttributeBatchUpdateProcessor; +use App\Api\Processor\AssetElasticsearchDocumentSyncProcessor; use App\Api\Processor\CopyAssetProcessor; use App\Api\Processor\MoveAssetProcessor; use App\Api\Processor\MultipleAssetCreateProcessor; @@ -161,10 +162,15 @@ ), new Get( uriTemplate: '/assets/{id}/es-document', - output: ESDocumentOutput::class, + output: ESDocumentStateOutput::class, name: 'es_document', provider: AssetElasticsearchDocumentProvider::class, ), + new Post( + uriTemplate: '/assets/{id}/es-document-sync', + name: 'sync_es_document', + processor: AssetElasticsearchDocumentSyncProcessor::class, + ), ], normalizationContext: [ 'groups' => [self::GROUP_LIST], diff --git a/databox/client/src/api/asset.ts b/databox/client/src/api/asset.ts index 8cd05aa4d..03290651c 100644 --- a/databox/client/src/api/asset.ts +++ b/databox/client/src/api/asset.ts @@ -1,5 +1,5 @@ import apiClient from './api-client'; -import {Asset, AssetFileVersion, Attribute, Collection, Share} from '../types'; +import {Asset, AssetFileVersion, Attribute, Collection, ESDocumentState, Share} from '../types'; import {ApiCollectionResponse, getAssetsHydraCollection, getHydraCollection,} from './hydra'; import {AxiosRequestConfig} from 'axios'; import {TFacets} from '../components/Media/Asset/Facets'; @@ -95,8 +95,12 @@ export async function getAsset(id: string): Promise { return (await apiClient.get(`/assets/${id}`)).data; } -export async function getAssetESDocument(id: string): Promise { - return (await apiClient.get(`/assets/${id}/es-document`)).data.data; +export async function getAssetESDocument(id: string): Promise { + return (await apiClient.get(`/assets/${id}/es-document`)).data; +} + +export async function syncAssetESDocument(id: string): Promise { + await apiClient.post(`/assets/${id}/es-document-sync`, {}); } export async function getAssetShares(assetId: string): Promise { diff --git a/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx b/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx index 42172d0c4..be8db3017 100644 --- a/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx +++ b/databox/client/src/components/Dialog/Asset/AssetESDocument.tsx @@ -1,10 +1,12 @@ -import {Asset, StateSetter} from '../../../types'; +import {Asset, ESDocumentState, StateSetter} from '../../../types'; import {DialogTabProps} from '../Tabbed/TabbedDialog'; import ContentTab from '../Tabbed/ContentTab'; -import {getAssetESDocument} from '../../../api/asset'; +import {getAssetESDocument, syncAssetESDocument} from '../../../api/asset'; +import {useTranslation} from 'react-i18next'; import {useCallback, useEffect, useState} from "react"; import RefreshIcon from '@mui/icons-material/Refresh'; -import {IconButton, LinearProgress, Typography} from "@mui/material"; +import {Alert, Button} from "@mui/material"; +import {LoadingButton} from "@mui/lab"; type Props = { data: Asset; @@ -16,8 +18,10 @@ export default function AssetESDocument({ onClose, minHeight, }: Props) { - const [document, setDocument] = useState(); + const {t} = useTranslation(); + const [document, setDocument] = useState(); const [loading, setLoading] = useState(false); + const [synced, setSynced] = useState(false); const refresh = useCallback(async () => { setLoading(true); @@ -32,17 +36,49 @@ export default function AssetESDocument({ refresh(); }, [refresh]); + const sync = async () => { + setSynced(true); + try { + await syncAssetESDocument(data.id) + } catch (e) { + setSynced(false); + } + } + return ( - - {loading && } + + } + > + {t('asset.es_document.refresh', 'Refresh')} + + } + > + {document ? <> - - - + {!document.synced ? + {synced ? t('asset.es_document.sync_scheduled', 'Sync scheduled') : t('asset.es_document.sync_now', 'Sync Now')} + } + > + {t('asset.es_document.not_synced', 'This document is not synced.')} + : null}
-                    {JSON.stringify(document, null, 4)}
+                    {JSON.stringify(document.data, null, 4)}
                 
: null}
diff --git a/databox/client/src/components/Dialog/Tabbed/ContentTab.tsx b/databox/client/src/components/Dialog/Tabbed/ContentTab.tsx index 833a72090..54bfa80e2 100644 --- a/databox/client/src/components/Dialog/Tabbed/ContentTab.tsx +++ b/databox/client/src/components/Dialog/Tabbed/ContentTab.tsx @@ -1,5 +1,5 @@ import {Button, Container, LinearProgress} from '@mui/material'; -import {PropsWithChildren} from 'react'; +import {PropsWithChildren, ReactNode} from 'react'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import {useTranslation} from 'react-i18next'; @@ -10,6 +10,7 @@ type Props = PropsWithChildren<{ minHeight?: number | undefined; disableGutters?: boolean; disablePadding?: boolean; + actions?: ReactNode; }>; export default function ContentTab({ @@ -19,6 +20,7 @@ export default function ContentTab({ minHeight, disableGutters, disablePadding, + actions, }: Props) { const {t} = useTranslation(); const progressHeight = 3; @@ -47,6 +49,7 @@ export default function ContentTab({ /> )} + {actions} diff --git a/databox/client/src/types.ts b/databox/client/src/types.ts index 21a01a812..34c7ada6b 100644 --- a/databox/client/src/types.ts +++ b/databox/client/src/types.ts @@ -47,6 +47,11 @@ export type Share = { alternateUrls: ShareAlternateUrl[]; }; +export type ESDocumentState = { + synced: boolean; + data: object; +} + export interface Asset extends IPermissions<{ canEditAttributes: boolean;