diff --git a/backend/src/services/file-query.service.ts b/backend/src/services/file-query.service.ts index 53415e51a..79850c177 100644 --- a/backend/src/services/file-query.service.ts +++ b/backend/src/services/file-query.service.ts @@ -257,6 +257,18 @@ export class FileQueryService { } } + if (query.includeStates && query.includeStates.length > 0) { + idQuery.andWhere('file.state IN (:...includeStates)', { + includeStates: query.includeStates, + }); + } + + if (query.excludeStates && query.excludeStates.length > 0) { + idQuery.andWhere('file.state NOT IN (:...excludeStates)', { + excludeStates: query.excludeStates, + }); + } + const categoryUUIDs = query.categories ? query.categories.split(',') : []; diff --git a/cli/kleinkram/api/query.py b/cli/kleinkram/api/query.py index 6bcbcf4de..ce4f0d11a 100644 --- a/cli/kleinkram/api/query.py +++ b/cli/kleinkram/api/query.py @@ -40,6 +40,8 @@ class FileQuery: patterns: List[str] = field(default_factory=list) ids: List[UUID] = field(default_factory=list) mission_query: MissionQuery = field(default_factory=MissionQuery) + include_states: List[str] = field(default_factory=list) + exclude_states: List[str] = field(default_factory=list) @dataclass diff --git a/cli/kleinkram/api/routes.py b/cli/kleinkram/api/routes.py index 3892e0c43..58fe45f1e 100644 --- a/cli/kleinkram/api/routes.py +++ b/cli/kleinkram/api/routes.py @@ -110,6 +110,8 @@ class Params(str, Enum): MISSION_IDS = "missionUuids" PROJECT_PATTERNS = "projectPatterns" PROJECT_IDS = "projectUuids" + INCLUDE_STATES = "includeStates" + EXCLUDE_STATES = "excludeStates" def _handle_list_params(params: Dict[str, Any]) -> Dict[str, Any]: @@ -151,6 +153,10 @@ def _file_query_to_params(file_query: FileQuery) -> Dict[str, List[str]]: params[Params.FILE_PATTERNS.value] = list(file_query.patterns) if file_query.ids: params[Params.FILE_IDS.value] = list(map(str, file_query.ids)) + if file_query.include_states: + params[Params.INCLUDE_STATES.value] = file_query.include_states + if file_query.exclude_states: + params[Params.EXCLUDE_STATES.value] = file_query.exclude_states return params diff --git a/cli/kleinkram/cli/_file.py b/cli/kleinkram/cli/_file.py index 6db52896e..b98a8002e 100644 --- a/cli/kleinkram/cli/_file.py +++ b/cli/kleinkram/cli/_file.py @@ -14,6 +14,7 @@ from kleinkram.api.routes import get_file from kleinkram.api.routes import get_files from kleinkram.config import get_shared_state +from kleinkram.models import FileState from kleinkram.printing import print_file_info from kleinkram.printing import print_files from kleinkram.utils import split_args @@ -91,8 +92,21 @@ def list_files( None, help="file names, ids or patterns", ), - projects: Optional[List[str]] = typer.Option(None, "--project", "-p", help="project name or id"), - missions: Optional[List[str]] = typer.Option(None, "--mission", "-m", help="mission name or id"), + projects: Optional[List[str]] = typer.Option( + None, "--project", "-p", help="project name or id. Repeat flag for multiple values" + ), + missions: Optional[List[str]] = typer.Option( + None, "--mission", "-m", help="mission name or id. Repeat flag for multiple values" + ), + include_canceled: bool = typer.Option(False, "--include-canceled", help="include canceled files"), + include_states: Optional[List[FileState]] = typer.Option( + None, + "--include-states", + help="file states to include. Repeat flag for multiple (e.g. --include-states OK --include-states FOUND)", + ), + exclude_states: Optional[List[FileState]] = typer.Option( + None, "--exclude-states", help="file states to exclude. Repeat flag for multiple values" + ), ) -> None: file_ids, file_patterns = split_args(files or []) mission_ids, mission_patterns = split_args(missions or []) @@ -104,7 +118,15 @@ def list_files( ids=mission_ids, patterns=mission_patterns, ) - file_query = FileQuery(mission_query=mission_query, patterns=file_patterns, ids=file_ids) + file_query = FileQuery( + mission_query=mission_query, + patterns=file_patterns, + ids=file_ids, + include_states=[state.value for state in include_states] if include_states else [], + exclude_states=( + [state.value for state in exclude_states] if exclude_states else (["CANCELED"] if not include_canceled else []) + ), + ) client = AuthenticatedClient() parsed_files = list(get_files(client, file_query=file_query)) diff --git a/cli/kleinkram/cli/_list.py b/cli/kleinkram/cli/_list.py index 1c2e94032..ef588368b 100644 --- a/cli/kleinkram/cli/_list.py +++ b/cli/kleinkram/cli/_list.py @@ -23,7 +23,9 @@ def files( ) -> None: from kleinkram.cli._file import list_files - list_files(files=files, projects=projects, missions=missions) + list_files( + files=files, projects=projects, missions=missions, include_canceled=False, include_states=None, exclude_states=None + ) @list_typer.command() diff --git a/frontend/src/components/explorer-page/explorer-page-files-table.vue b/frontend/src/components/explorer-page/explorer-page-files-table.vue index 389ec7f3f..2b3ca53fe 100644 --- a/frontend/src/components/explorer-page/explorer-page-files-table.vue +++ b/frontend/src/components/explorer-page/explorer-page-files-table.vue @@ -214,7 +214,7 @@ import type { CategoryDto } from '@kleinkram/api-dto/types/category.dto'; import type { FileWithTopicDto } from '@kleinkram/api-dto/types/file/file.dto'; import type { FilesDto } from '@kleinkram/api-dto/types/file/files.dto'; -import { FileType, HealthStatus } from '@kleinkram/shared'; +import { FileState, FileType, HealthStatus } from '@kleinkram/shared'; import { keepPreviousData, useQuery, @@ -381,6 +381,8 @@ const { ? h.searchParams.messageDatatypes.split(',') : undefined, h.searchParams.matchAllTopics === 'true', + undefined, + [FileState.CANCELED], ); }, placeholderData: keepPreviousData, diff --git a/frontend/src/services/queries/file.ts b/frontend/src/services/queries/file.ts index 2a58726f5..e1baf26c0 100644 --- a/frontend/src/services/queries/file.ts +++ b/frontend/src/services/queries/file.ts @@ -6,7 +6,7 @@ import type { FilesDto } from '@kleinkram/api-dto/types/file/files.dto'; import type { FoxgloveLinkResponseDto } from '@kleinkram/api-dto/types/file/foxglove-link-response.dto'; import type { IsUploadingDto } from '@kleinkram/api-dto/types/file/is-uploading.dto'; import type { StorageOverviewDto } from '@kleinkram/api-dto/types/storage-overview.dto'; -import { FileType, HealthStatus } from '@kleinkram/shared'; +import { FileState, FileType, HealthStatus } from '@kleinkram/shared'; import { AxiosResponse } from 'axios'; import axios from 'src/api/axios'; @@ -27,6 +27,8 @@ export interface FilteredFilesConfig { sort?: string | undefined; desc?: boolean | undefined; health?: HealthStatus | undefined; + includeStates?: FileState[] | undefined; + excludeStates?: FileState[] | undefined; } export const fetchFilteredFiles = async ( @@ -50,6 +52,8 @@ export const fetchFilteredFiles = async ( sort, desc, health, + includeStates, + excludeStates, } = config; try { const parameters: Record = {}; @@ -74,8 +78,15 @@ export const fetchFilteredFiles = async ( if (desc !== undefined) parameters.sortDirection = desc ? 'DESC' : 'ASC'; if (health) parameters.health = health; + const stateParameters = new URLSearchParams(parameters); + for (const state of includeStates ?? []) { + stateParameters.append('includeStates', state); + } + for (const state of excludeStates ?? []) { + stateParameters.append('excludeStates', state); + } - const queryParameters = new URLSearchParams(parameters).toString(); + const queryParameters = stateParameters.toString(); const response: AxiosResponse = await axios.get( `/files?${queryParameters}`, ); @@ -131,6 +142,8 @@ export const filesOfMission = async ( topics?: string[], messageDatatypes?: string[], matchAllTopics = true, + includeStates?: FileState[], + excludeStates?: FileState[], ): Promise => { const tag: Record = {}; @@ -150,6 +163,8 @@ export const filesOfMission = async ( endDate, topics, messageDatatypes, + includeStates, + excludeStates, }); }; diff --git a/packages/api-dto/src/types/file/file-query.dto.ts b/packages/api-dto/src/types/file/file-query.dto.ts index 067e778c1..782149437 100644 --- a/packages/api-dto/src/types/file/file-query.dto.ts +++ b/packages/api-dto/src/types/file/file-query.dto.ts @@ -1,5 +1,5 @@ import { MissionQueryDto } from '@api-dto/mission/mission-query.dto'; -import { HealthStatus } from '@kleinkram/shared'; +import { FileState, HealthStatus } from '@kleinkram/shared'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { @@ -176,4 +176,32 @@ export class FileQueryDto extends MissionQueryDto { @IsString() @ApiProperty({ required: false }) sort?: string; + + @IsOptional() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + @Transform(({ value }) => (Array.isArray(value) ? value : [value])) + @IsArray() + @ArrayNotEmpty() + @IsEnum(FileState, { each: true }) + @ApiProperty({ + required: false, + isArray: true, + enum: FileState, + description: 'File states to include (whitelist)', + }) + includeStates?: FileState[]; + + @IsOptional() + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + @Transform(({ value }) => (Array.isArray(value) ? value : [value])) + @IsArray() + @ArrayNotEmpty() + @IsEnum(FileState, { each: true }) + @ApiProperty({ + required: false, + isArray: true, + enum: FileState, + description: 'File states to exclude (blacklist)', + }) + excludeStates?: FileState[]; }