import { faClock, faCloud, faExclamationTriangle, faLock, faPause, faSpinner, type IconDefinition, } from "@fortawesome/free-solid-svg-icons"; import type { DCESummary, HDADetailed, HDCADetailed } from "@/api"; import { ERROR_DATASET_STATES, NON_TERMINAL_DATASET_STATES } from "@/api/datasets"; import type { HistoryContentsResult } from "@/api/histories"; import type { components } from "@/api/schema"; import { DatasetStateSummary } from "../Collection/DatasetStateSummary"; type HistoryContentItem = HistoryContentsResult[number]; function isHDCAItem(item: HistoryContentItem | HDADetailed | HDCADetailed | DCESummary): boolean { return ( item && typeof item === "object" && "history_content_type" in item && item.history_content_type === "dataset_collection" ); } function isDCEWithCollection(item: HistoryContentItem | HDADetailed | HDCADetailed | DCESummary): boolean { return item && typeof item === "object" && "collection_type" in item && item.collection_type !== undefined; } type DatasetState = components["schemas"]["DatasetState"]; // The 'failed' state is for the collection job state summary, not a dataset state. export type State = | DatasetState | "failed" | "placeholder" | "failed_populated_state" | "new_populated_state" | "inaccessible"; interface StateRepresentation { status: "success" | "warning" | "info" | "danger" | "secondary"; text?: string; displayName?: string; icon?: IconDefinition; spin?: boolean; nonDb?: boolean; } export type StateMap = { [__ in State]: StateRepresentation; }; /** * Client representation of state and state messages. * See: https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/model/__init__.py#L3292 for a list of available states. */ export const STATES: StateMap = { /** has successfully completed running */ ok: { status: "success", displayName: "ok", }, /** has no data */ empty: { status: "success", text: "No data.", displayName: "empty", }, /** was created without a tool */ new: { status: "warning", text: "This is a new dataset and not all of its data are available yet.", displayName: "new", icon: faClock, }, /** the job that will produce the dataset queued in the runner */ queued: { status: "warning", text: "This job is waiting to run.", displayName: "queued", icon: faClock, }, /** the job that will produce the dataset is running */ running: { status: "warning", text: "This job is currently running.", displayName: "running", icon: faSpinner, spin: true, }, /** metadata for the dataset is being discovered/set */ setting_metadata: { status: "warning", text: "Metadata is being auto-detected.", displayName: "setting metadata", icon: faSpinner, spin: true, }, /** is uploading and not ready */ upload: { status: "warning", text: "This dataset is currently uploading.", displayName: "uploading", icon: faSpinner, spin: true, }, /** remote dataset */ deferred: { status: "info", text: "This dataset is remote, has not been ingested by Galaxy, and full metadata may not be available.", displayName: "deferred", icon: faCloud, }, /** the job that will produce the dataset paused */ paused: { status: "info", text: "This job is paused. Use the 'Resume Paused Jobs' in the history menu to resume.", displayName: "paused", icon: faPause, }, /** deleted while uploading */ discarded: { status: "danger", text: "This dataset is discarded - the job creating it may have been cancelled or it may have been imported without file data.", displayName: "discarded", icon: faExclamationTriangle, }, /** the tool producing this dataset has errored */ error: { status: "danger", text: "An error occurred with this dataset.", displayName: "error", icon: faExclamationTriangle, }, /** metadata discovery/setting failed or errored (but otherwise ok) */ failed_metadata: { status: "danger", text: "Metadata generation failed. Please retry.", displayName: "failed metadata", icon: faExclamationTriangle, }, /** the job has failed, this is not a dataset but a job state used in the collection job state summary. */ failed: { status: "danger", displayName: "failed", icon: faExclamationTriangle, }, /** the dataset is not yet loaded in the UI. This state is only visual and transitional, it does not exist in the database. */ placeholder: { status: "secondary", text: "This dataset is being fetched.", displayName: "loading", icon: faSpinner, spin: true, nonDb: true, }, /** the `populated_state: failed`. This state is only visual and transitional, it does not exist in the database. */ failed_populated_state: { status: "danger", text: "Failed to populate the collection.", displayName: "failed", icon: faExclamationTriangle, nonDb: true, }, /** the `populated_state: new`. This state is only visual and transitional, it does not exist in the database. */ new_populated_state: { status: "warning", text: "This is a new collection and not all of its data are available yet.", displayName: "new", icon: faClock, nonDb: true, }, inaccessible: { status: "warning", text: "User not allowed to access this dataset.", displayName: "inaccessible", icon: faLock, nonDb: true, }, } as const satisfies StateMap; /** We want to display a single state for a dataset collection whose elements may have mixed states. * This list is ordered from highest to lowest priority. If any element is in error state the whole collection should be in error. */ export const HIERARCHICAL_COLLECTION_JOB_STATES = [ "error", "failed", "upload", "paused", "running", "queued", "new", ] as const; /** Similar hierarchy for dataset states, ordered from highest to lowest priority. */ export const HIERARCHICAL_COLLECTION_DATASET_STATES = [ ...(ERROR_DATASET_STATES as readonly DatasetState[]), ...(NON_TERMINAL_DATASET_STATES as readonly DatasetState[]), "deferred" as const, "discarded" as const, ] as const satisfies readonly DatasetState[]; export function getContentItemState(item: HistoryContentItem | HDADetailed | HDCADetailed): State { if (isHDCAItem(item) || isDCEWithCollection(item)) { if ("populated_state" in item && item.populated_state === "failed") { return "failed_populated_state"; } if ("populated_state" in item && item.populated_state === "new") { return "new_populated_state"; } // Check dataset states first (higher priority for actual data states) if ("elements_states" in item && item.elements_states) { const datasetSummary = new DatasetStateSummary( item as { elements_states?: Record; elements_deleted?: number; populated_state?: string | null; }, ); for (const datasetState of HIERARCHICAL_COLLECTION_DATASET_STATES) { if (datasetSummary.get(datasetState) > 0) { return datasetState; } } } // Fall back to job states if no dataset states if ("job_state_summary" in item && item.job_state_summary) { const jobStateSummary = item.job_state_summary as Record; for (const jobState of HIERARCHICAL_COLLECTION_JOB_STATES) { if ((jobStateSummary[jobState] || 0) > 0) { return jobState; } } } } else if ("accessible" in item && item.accessible === false) { return "inaccessible"; } else if ("state" in item && item.state) { return item.state; } return "ok"; }