feat(preview): expose availability and handle missing services
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -29,6 +29,29 @@ final readonly class PreviewManager
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array{enabled:bool,storage:bool,generation:bool} */
|
||||||
|
public function availability(string $tenantId): array
|
||||||
|
{
|
||||||
|
$tenant = $this->tenants->fetchById($tenantId);
|
||||||
|
if ($tenant === null) {
|
||||||
|
return ['enabled' => false, 'storage' => false, 'generation' => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration = $tenant->getConfiguration()->preview();
|
||||||
|
$enabled = $configuration->enabled();
|
||||||
|
$storage = $enabled
|
||||||
|
&& $tenant->getConfiguration()->stores()->store($configuration->store()) !== null;
|
||||||
|
$generation = $storage && $this->previewProviders() !== [];
|
||||||
|
|
||||||
|
if ($enabled && !$storage) {
|
||||||
|
$this->logger->debug('Preview storage is not configured', ['tenantId' => $tenantId]);
|
||||||
|
} elseif ($enabled && $storage && !$generation) {
|
||||||
|
$this->logger->debug('No preview generation provider is available', ['tenantId' => $tenantId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compact('enabled', 'storage', 'generation');
|
||||||
|
}
|
||||||
|
|
||||||
public function fetch(
|
public function fetch(
|
||||||
string $tenantId,
|
string $tenantId,
|
||||||
PreviewSource $source,
|
PreviewSource $source,
|
||||||
@@ -45,6 +68,7 @@ final readonly class PreviewManager
|
|||||||
!$configuration->enabled()
|
!$configuration->enabled()
|
||||||
|| $variantConfiguration === null
|
|| $variantConfiguration === null
|
||||||
|| ($source->size !== null && $source->size > $configuration->maxSourceSize())
|
|| ($source->size !== null && $source->size > $configuration->maxSourceSize())
|
||||||
|
|| $tenant->getConfiguration()->stores()->store($configuration->store()) === null
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -69,6 +93,9 @@ final readonly class PreviewManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$provider = $this->selectProvider($source->mimeType, $request);
|
$provider = $this->selectProvider($source->mimeType, $request);
|
||||||
|
if ($provider === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$result = $provider->generate($source, $request);
|
$result = $provider->generate($source, $request);
|
||||||
$blob = $this->writeCache(
|
$blob = $this->writeCache(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
@@ -218,11 +245,11 @@ final readonly class PreviewManager
|
|||||||
private function selectProvider(
|
private function selectProvider(
|
||||||
string $sourceMimeType,
|
string $sourceMimeType,
|
||||||
PreviewRequest $request,
|
PreviewRequest $request,
|
||||||
): PreviewProviderInterface {
|
): ?PreviewProviderInterface {
|
||||||
$sourceMimeType = MimeType::normalize($sourceMimeType);
|
$sourceMimeType = MimeType::normalize($sourceMimeType);
|
||||||
$candidates = [];
|
$candidates = [];
|
||||||
|
|
||||||
foreach ($this->providers->providers(ProviderInterface::TYPE_PREVIEW) as $identifier => $provider) {
|
foreach ($this->previewProviders() as $identifier => $provider) {
|
||||||
if (
|
if (
|
||||||
!$provider instanceof PreviewProviderInterface
|
!$provider instanceof PreviewProviderInterface
|
||||||
|| !$provider->supports($sourceMimeType, $request)
|
|| !$provider->supports($sourceMimeType, $request)
|
||||||
@@ -234,9 +261,7 @@ final readonly class PreviewManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($candidates === []) {
|
if ($candidates === []) {
|
||||||
throw new PreviewGenerationException(
|
return null;
|
||||||
"No preview provider supports {$sourceMimeType} to {$request->preferredMimeType}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usort($candidates, static function (array $left, array $right): int {
|
usort($candidates, static function (array $left, array $right): int {
|
||||||
@@ -246,4 +271,13 @@ final readonly class PreviewManager
|
|||||||
|
|
||||||
return $candidates[0]['provider'];
|
return $candidates[0]['provider'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, PreviewProviderInterface> */
|
||||||
|
private function previewProviders(): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->providers->providers(ProviderInterface::TYPE_PREVIEW),
|
||||||
|
static fn(ProviderInterface $provider): bool => $provider instanceof PreviewProviderInterface,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createPinia } from 'pinia'
|
|||||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||||
import { useModuleStore } from '@KTXC/stores/moduleStore'
|
import { useModuleStore } from '@KTXC/stores/moduleStore'
|
||||||
import { useTenantStore } from '@KTXC/stores/tenantStore'
|
import { useTenantStore } from '@KTXC/stores/tenantStore'
|
||||||
|
import { usePreviewStore } from '@KTXC/stores/previewStore'
|
||||||
import { useUserStore } from '@KTXC/stores/userStore'
|
import { useUserStore } from '@KTXC/stores/userStore'
|
||||||
import { useL10nStore } from '@KTXC/stores/l10nStore'
|
import { useL10nStore } from '@KTXC/stores/l10nStore'
|
||||||
import { useThemeStore } from '@KTXC/stores/themeStore'
|
import { useThemeStore } from '@KTXC/stores/themeStore'
|
||||||
@@ -41,6 +42,7 @@ globalWindow.Pinia = PiniaLib as unknown
|
|||||||
(async () => {
|
(async () => {
|
||||||
const moduleStore = useModuleStore();
|
const moduleStore = useModuleStore();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
|
const previewStore = usePreviewStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const l10nStore = useL10nStore();
|
const l10nStore = useL10nStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -49,6 +51,7 @@ globalWindow.Pinia = PiniaLib as unknown
|
|||||||
try {
|
try {
|
||||||
const payload = await fetchWrapper.get('/init');
|
const payload = await fetchWrapper.get('/init');
|
||||||
moduleStore.init(payload?.modules ?? {});
|
moduleStore.init(payload?.modules ?? {});
|
||||||
|
previewStore.init(payload?.preview ?? null);
|
||||||
tenantStore.init(payload?.tenant ?? null);
|
tenantStore.init(payload?.tenant ?? null);
|
||||||
userStore.init(payload?.user ?? {});
|
userStore.init(payload?.user ?? {});
|
||||||
layoutStore.hydrateFromSettings();
|
layoutStore.hydrateFromSettings();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
// Stores
|
// Stores
|
||||||
export { useModuleStore } from '../stores/moduleStore'
|
export { useModuleStore } from '../stores/moduleStore'
|
||||||
export { useTenantStore } from '../stores/tenantStore'
|
export { useTenantStore } from '../stores/tenantStore'
|
||||||
|
export { usePreviewStore } from '../stores/previewStore'
|
||||||
|
export type { PreviewAvailability } from '../stores/previewStore'
|
||||||
export { useUserStore } from '../stores/userStore'
|
export { useUserStore } from '../stores/userStore'
|
||||||
export { useIntegrationStore } from '../stores/integrationStore'
|
export { useIntegrationStore } from '../stores/integrationStore'
|
||||||
export { useLayoutStore } from '../stores/layoutStore'
|
export { useLayoutStore } from '../stores/layoutStore'
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface PreviewAvailability {
|
||||||
|
enabled: boolean
|
||||||
|
storage: boolean
|
||||||
|
generation: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const unavailable = (): PreviewAvailability => ({
|
||||||
|
enabled: false,
|
||||||
|
storage: false,
|
||||||
|
generation: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const usePreviewStore = defineStore('previewStore', () => {
|
||||||
|
const availability = ref<PreviewAvailability>(unavailable())
|
||||||
|
|
||||||
|
function init(data?: Partial<PreviewAvailability> | null): void {
|
||||||
|
availability.value = {
|
||||||
|
enabled: data?.enabled ?? false,
|
||||||
|
storage: data?.storage ?? false,
|
||||||
|
generation: data?.generation ?? false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(): void {
|
||||||
|
availability.value = unavailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
availability,
|
||||||
|
init,
|
||||||
|
reset,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -154,12 +154,7 @@ final class PreviewManagerTest extends TestCase
|
|||||||
$service = $this->createMock(SystemStoreServiceInterface::class);
|
$service = $this->createMock(SystemStoreServiceInterface::class);
|
||||||
$service->expects(self::once())->method('stat')->willReturn(null);
|
$service->expects(self::once())->method('stat')->willReturn(null);
|
||||||
$logger = $this->createMock(LoggerInterface::class);
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
$logger->expects(self::once())->method('warning')->with(
|
$logger->expects(self::never())->method('warning');
|
||||||
'Preview unavailable',
|
|
||||||
self::callback(static fn(array $context): bool =>
|
|
||||||
($context['exception'] ?? null) instanceof PreviewGenerationException
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertNull($this->manager(
|
self::assertNull($this->manager(
|
||||||
$provider,
|
$provider,
|
||||||
@@ -169,6 +164,50 @@ final class PreviewManagerTest extends TestCase
|
|||||||
)->fetch('tenant-a', $this->source()));
|
)->fetch('tenant-a', $this->source()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function missingPreviewStorageIsUnavailableWithoutLoggingOrProviderLookup(): void
|
||||||
|
{
|
||||||
|
$provider = $this->provider();
|
||||||
|
$provider->expects(self::never())->method('generate');
|
||||||
|
$providerManager = $this->createMock(ProviderManager::class);
|
||||||
|
$providerManager->expects(self::never())->method('providers');
|
||||||
|
$service = $this->createMock(SystemStoreServiceInterface::class);
|
||||||
|
$service->expects(self::never())->method('stat');
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$logger->expects(self::never())->method('warning');
|
||||||
|
|
||||||
|
self::assertNull($this->manager(
|
||||||
|
$provider,
|
||||||
|
$service,
|
||||||
|
new TenantConfiguration(),
|
||||||
|
$providerManager,
|
||||||
|
$logger,
|
||||||
|
)->fetch('tenant-a', $this->source()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function reportsStorageAndGenerationAvailabilitySeparately(): void
|
||||||
|
{
|
||||||
|
$provider = $this->provider();
|
||||||
|
$providerManager = $this->createMock(ProviderManager::class);
|
||||||
|
$providerManager->expects(self::once())->method('providers')->willReturn([]);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$logger->expects(self::once())->method('debug')->with(
|
||||||
|
'No preview generation provider is available',
|
||||||
|
['tenantId' => 'tenant-a'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['enabled' => true, 'storage' => true, 'generation' => false],
|
||||||
|
$this->manager(
|
||||||
|
$provider,
|
||||||
|
$this->createStub(SystemStoreServiceInterface::class),
|
||||||
|
providerManager: $providerManager,
|
||||||
|
logger: $logger,
|
||||||
|
)->availability('tenant-a'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function cacheKeysAreDeterministicOpaqueAndChangeWithSourceIdentity(): void
|
public function cacheKeysAreDeterministicOpaqueAndChangeWithSourceIdentity(): void
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user