feat: image variants cache panel

This commit is contained in:
fsylvain committed 2026-10-08 21:13:32 +02:00
1 parent 951fef18ba
commit d768668014
53 files changed
+5809 -46

No files matched your search

@@ -0,0 +1,111 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { initializeLocale } from '@/i18n'
import type { ActivityItem } from '@/lib/activity'
import { detailLine, sentenceText } from '../activitySentence'
function purgeItem(overrides: Partial<ActivityItem>): ActivityItem {
return {
id: 1,
created_at: '2026-10-03T12:00:00Z',
action: 'media.variant_purge_all',
category: 'media',
actor: { type: 'user', id: 3, name: 'Marta Alves' },
target: null,
outcome: 'success',
failure_reason: null,
security: false,
source: 'web',
ip: null,
user_agent: null,
params: {},
changes: [],
...overrides,
}
}
const hero = { type: 'image_variant', id: 'hero', name: 'hero', exists: true }
const wall = { type: 'image', id: '1', name: 'north-wall.jpg', exists: true }
const rows: [string, Partial<ActivityItem>, string, string | null][] = [
[
'media.variant_purge_all success',
{ params: { purged_files: 1284, purged_bytes: 222298112 } },
'Purged all variants',
'Removed 1,284 files (212 MB)',
],
['media.variant_purge_all failed', { outcome: 'failed' }, 'Failed to purge all variants', null],
[
'media.variant_purge_unused success',
{
action: 'media.variant_purge_unused',
params: { purged_files: 312, purged_bytes: 42991616, older_than_days: 30 },
},
'Purged unused variants',
'Removed 312 files (41 MB) not requested for 30 days',
],
[
'media.variant_purge_unused failed',
{ action: 'media.variant_purge_unused', outcome: 'failed' },
'Failed to purge unused variants',
null,
],
[
'media.variant_purge success',
{
action: 'media.variant_purge',
target: hero,
params: { purged_files: 244, purged_bytes: 88080384 },
},
'Purged the cache of variant “hero”',
'Removed 244 files (84 MB)',
],
[
'media.variant_purge failed',
{ action: 'media.variant_purge', target: hero, outcome: 'failed' },
'Failed to purge the cache of variant “hero”',
null,
],
[
'media.variant_purge_image success',
{
action: 'media.variant_purge_image',
target: wall,
params: { purged_files: 6, purged_bytes: 6082765 },
},
'Purged the variants of north-wall.jpg',
'Removed 6 files (5.8 MB)',
],
]
beforeEach(() => {
localStorage.clear()
initializeLocale()
})
afterEach(() => {
localStorage.clear()
initializeLocale()
})
describe('The Activity screen words each cache purge scope', () => {
it.each(rows)('%s', (_, overrides, sentence, detail) => {
const subject = purgeItem(overrides)
expect(sentenceText(subject)).toBe(sentence)
expect(detailLine(subject)).toBe(detail)
})
it('A partial purge adds the failed count to the detail line', () => {
const subject = purgeItem({
params: { purged_files: 640, purged_bytes: 111149056, failed_files: 644 },
})
expect(detailLine(subject)).toBe('Removed 640 files (106 MB) · 644 failed')
})
it('The singular reads 1 file', () => {
const subject = purgeItem({ params: { purged_files: 1, purged_bytes: 2048 } })
expect(detailLine(subject)).toBe('Removed 1 file (2 KB)')
})
})
@@ -231,6 +231,9 @@ describe('The catalogue is complete in both languages', () => {
'media.merge',
'media.purge',
'media.variant_generate_all',
'media.variant_purge',
'media.variant_purge_all',
'media.variant_purge_unused',
'media.variant_purge_image',
'system.media_purge',
'user.register',
+48
View File
@@ -3,6 +3,7 @@ import type { MessageKey } from '@/i18n/messages'
import type { ActivityItem } from '@/lib/activity'
import { sizeLabel } from '@/lib/activityRetention'
import { formatSize } from '@/lib/mediaFormat'
import { formatVariantSize } from '@/lib/variants/variantFormat'
import { causeText } from '@/lib/variants/variantCauses'
export interface SentenceSegment {
@@ -62,6 +63,18 @@ const CATALOGUE: Record<string, Template> = {
success: 'Generated all variants of {0}',
failed: 'Failed to generate the variants of {0}',
},
'media.variant_purge': {
success: 'Purged the cache of variant “{0}”',
failed: 'Failed to purge the cache of variant “{0}”',
},
'media.variant_purge_all': {
success: 'Purged all variants',
failed: 'Failed to purge all variants',
},
'media.variant_purge_unused': {
success: 'Purged unused variants',
failed: 'Failed to purge unused variants',
},
'media.variant_purge_image': {
success: 'Purged the variants of {0}',
failed: 'Failed to purge the variants of {0}',
@@ -262,7 +275,42 @@ function purgeImageDetail(params: Record<string, unknown>) {
: t('Removed {0} files ({1})', [formatNumber(files), size])
}
const DEFAULT_UNUSED_DAYS = 30
const CACHE_PURGE_ACTIONS = [
'media.variant_purge',
'media.variant_purge_all',
'media.variant_purge_unused',
]
function removedFiles(files: number, size: string, days: number | null) {
if (days === null) {
return files === 1
? t('Removed 1 file ({0})', [size])
: t('Removed {0} files ({1})', [formatNumber(files), size])
}
return files === 1
? t('Removed 1 file ({0}) not requested for {1} days', [size, days])
: t('Removed {0} files ({1}) not requested for {2} days', [formatNumber(files), size, days])
}
function cachePurgeDetail(item: ActivityItem) {
if (item.outcome === 'failed') return null
const params = item.params
const unused = item.action === 'media.variant_purge_unused'
const days = unused ? count(params.older_than_days) || DEFAULT_UNUSED_DAYS : null
const removed = removedFiles(
count(params.purged_files),
formatVariantSize(count(params.purged_bytes)),
days,
)
const failed = count(params.failed_files)
return failed > 0 ? `${removed} · ${t('{0} failed', [formatNumber(failed)])}` : removed
}
export function detailLine(item: ActivityItem): string | null {
if (CACHE_PURGE_ACTIONS.includes(item.action)) {
return cachePurgeDetail(item)
}
if (item.action === 'media.variant_generate_all') {
return generateAllDetail(item.params)
}
+26
View File
@@ -0,0 +1,26 @@
import { formatNumber, t } from '@/i18n'
import { formatVariantSize } from '@/lib/variants/variantFormat'
const PERCENT = 100
export function cacheSize(bytes: number) {
return bytes === 0 ? t('{0} MB', [0]) : formatVariantSize(bytes)
}
export function filesCount(count: number) {
return count === 1 ? t('1 file') : t('{0} files', [formatNumber(count)])
}
export function filesAndSize(count: number, bytes: number) {
return count === 1
? t('1 file · {0}', [formatVariantSize(bytes)])
: t('{0} files · {1}', [formatNumber(count), formatVariantSize(bytes)])
}
export function variantsCount(count: number) {
return count === 1 ? t('1 variant') : t('{0} variants', [formatNumber(count)])
}
export function barPercent(bytes: number, largest: number) {
return largest === 0 ? 0 : Math.round((bytes / largest) * PERCENT)
}