From 8a01ddc040652a408e5b1269c2f484501a05e7ab Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 17 Dec 2025 17:27:17 -0700 Subject: [PATCH] Adding links between domain page and archives page. --- app/src/components/Archive.js | 75 +++++++++++++++++++++++++++++++++-- wrolpi/collections/lib.py | 66 +++++++++++++++++++++++------- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/app/src/components/Archive.js b/app/src/components/Archive.js index edeb6a86..e5c822f8 100644 --- a/app/src/components/Archive.js +++ b/app/src/components/Archive.js @@ -10,9 +10,14 @@ import { GridRow, Image, Input, + PlaceholderHeader, + PlaceholderLine, + StatisticLabel, + StatisticValue, TableCell, TextArea, } from "semantic-ui-react"; +import Icon from "semantic-ui-react/dist/commonjs/elements/Icon"; import { APIButton, BackButton, @@ -23,6 +28,7 @@ import { FileIcon, findPosterPath, humanFileSize, + humanNumber, InfoHeader, isoDatetimeToAgoPopup, mimetypeColor, @@ -48,12 +54,12 @@ import { import {CollectionTagModal} from "./collections/CollectionTagModal"; import {Link, Route, Routes, useLocation, useNavigate, useParams} from "react-router"; import Message from "semantic-ui-react/dist/commonjs/collections/Message"; -import {useArchive, useDomain, useDomains, useOneQuery, useSearchArchives, useSearchOrder} from "../hooks/customHooks"; +import {useArchive, useDomain, useDomains, useOneQuery, useSearchArchives, useSearchDomain, useSearchOrder} from "../hooks/customHooks"; import {FileCards, FileRowTagIcon, FilesView} from "./Files"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import _ from "lodash"; import {Media, ThemeContext} from "../contexts/contexts"; -import {Button, Card, darkTheme, Header, Loader, Popup, Segment, Tab} from "./Theme"; +import {Button, Card, darkTheme, Header, Loader, Placeholder, Popup, Segment, Statistic, Tab} from "./Theme"; import {taggedImageLabel, TagsSelector} from "../Tags"; import {toast} from "react-semantic-toasts-2"; import {API_ARCHIVE_UPLOAD_URI, Downloaders} from "./Vars"; @@ -384,6 +390,32 @@ const DOMAIN_ROUTES = { searchParam: 'domain' }; +function DomainStatistics({statistics}) { + if (!statistics) { + return <> + } + + return +
Statistics
+ + {statistics.archive_count} + Archives + + + {humanFileSize(statistics.size, true)} + Total Size + + + {humanFileSize(statistics.largest_archive, true)} + Largest Archive + + + {humanNumber(statistics.archive_tags)} + Archive Tags + +
+} + export function DomainsPage() { useTitle('Archive Domains'); @@ -565,6 +597,10 @@ export function DomainEditPage() { return <> + + + + + + {domain && domain.statistics && } ; } @@ -666,7 +704,16 @@ function ArchiveSettingsPage() { function ArchivesPage() { const [selectedArchives, setSelectedArchives] = useState([]); - useTitle('Archives'); + const {domain, domains} = useSearchDomain(); + + // Find the domain object from the domains list to get the ID for the edit link + const domainObj = domain && domains ? domains.find(d => d.domain === domain) : null; + + let title = 'Archives'; + if (domainObj && domainObj.domain) { + title = `${domainObj.domain} Archives`; + } + useTitle(title); const { archives, @@ -763,7 +810,29 @@ function ArchivesPage() { placeholder='Search Archives...' />; + // Domain header with edit link when filtering by domain + let header; + if (domainObj && domainObj.domain) { + const editLink = `/archive/domain/${domainObj.id}/edit`; + header = <> +
+ {domainObj.domain} + + + +
+ ; + } else if (domain) { + // Domain filter is set but domain object not loaded yet + header = + + + + ; + } + return <> + {header} diff --git a/wrolpi/collections/lib.py b/wrolpi/collections/lib.py index ca1756f0..ace3dc4a 100644 --- a/wrolpi/collections/lib.py +++ b/wrolpi/collections/lib.py @@ -25,6 +25,7 @@ logger = logger.getChild(__name__) __all__ = [ 'get_collections', 'get_collection_with_stats', + 'get_domain_statistics', 'update_collection', 'refresh_collection', 'tag_collection', @@ -162,21 +163,11 @@ def get_collection_with_stats(session: Session, collection_id: int) -> dict: # This can be extended for different collection types if collection.kind == 'domain': # Add archive statistics for domain collections - # Local imports to avoid circular import: collections -> archive -> collections - from modules.archive import Archive - from wrolpi.files.models import FileGroup - - stats_query = session.query( - func.count(Archive.id).label('archive_count'), - func.sum(FileGroup.size).label('size') - ).outerjoin( - FileGroup, FileGroup.id == Archive.file_group_id - ).filter( - Archive.collection_id == collection_id - ).one() - - data['archive_count'] = stats_query.archive_count or 0 - data['size'] = stats_query.size or 0 + # Use the detailed statistics function (similar to Channel.get_statistics()) + statistics = get_domain_statistics(session, collection_id) + data['archive_count'] = statistics['archive_count'] + data['size'] = statistics['size'] + data['statistics'] = statistics data['domain'] = data['name'] # Alias for backward compatibility elif collection.kind == 'channel': @@ -618,3 +609,48 @@ def search_collections( collections = query.all() return [collection.__json__() for collection in collections] + + +def get_domain_statistics(session: Session, collection_id: int) -> Dict: + """ + Get detailed statistics for a domain collection. + + Similar to Channel.get_statistics(), this returns comprehensive stats about + a domain collection's archives. + + Args: + session: Database session + collection_id: The domain collection ID + + Returns: + Dict with: + - archive_count: Number of archives in the domain + - size: Total size of all archive files + - largest_archive: Size of the largest archive + - archive_tags: Count of archives that have at least one tag + """ + # Local imports to avoid circular import: collections -> archive -> collections + from modules.archive.models import Archive + from wrolpi.files.models import FileGroup + from wrolpi.tags import TagFile + + # Use a single query with aggregate functions similar to Channel.get_statistics() + stats_query = session.query( + func.count(Archive.id).label('archive_count'), + func.coalesce(func.sum(FileGroup.size), 0).label('size'), + func.coalesce(func.max(FileGroup.size), 0).label('largest_archive'), + func.count(Archive.id).filter(TagFile.file_group_id.isnot(None)).label('archive_tags') + ).outerjoin( + FileGroup, FileGroup.id == Archive.file_group_id + ).outerjoin( + TagFile, FileGroup.id == TagFile.file_group_id + ).filter( + Archive.collection_id == collection_id + ).one() + + return { + 'archive_count': stats_query.archive_count or 0, + 'size': int(stats_query.size or 0), + 'largest_archive': int(stats_query.largest_archive or 0), + 'archive_tags': stats_query.archive_tags or 0, + }