use serde::{Deserialize, Serialize};
use crate::job::{Job, JobStatus};
use crate::job_rows::status_counts;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobColumn {
Id,
Name,
Status,
Time,
Partition,
Nodes,
Priority,
Reason,
Account,
Qos,
Cpus,
NodeList,
}
pub const ALL_COLUMNS: [JobColumn; 12] = [
JobColumn::Id,
JobColumn::Name,
JobColumn::Status,
JobColumn::Time,
JobColumn::Partition,
JobColumn::Nodes,
JobColumn::Priority,
JobColumn::Reason,
JobColumn::Account,
JobColumn::Qos,
JobColumn::Cpus,
JobColumn::NodeList,
];
impl JobColumn {
pub fn defaults() -> Vec<JobColumn> {
vec![
JobColumn::Id,
JobColumn::Name,
JobColumn::Status,
JobColumn::Time,
JobColumn::Partition,
JobColumn::Nodes,
]
}
pub fn config_name(&self) -> &'static str {
match self {
JobColumn::Id => "id",
JobColumn::Name => "name",
JobColumn::Status => "status",
JobColumn::Time => "time",
JobColumn::Partition => "partition",
JobColumn::Nodes => "nodes",
JobColumn::Priority => "priority",
JobColumn::Reason => "reason",
JobColumn::Account => "account",
JobColumn::Qos => "qos",
JobColumn::Cpus => "cpus",
JobColumn::NodeList => "nodelist",
}
}
pub fn header(&self) -> &'static str {
match self {
JobColumn::Id => "ID",
JobColumn::Name => "Name",
JobColumn::Status => "Status",
JobColumn::Time => "Time",
JobColumn::Partition => "Partition",
JobColumn::Nodes => "Nodes",
JobColumn::Priority => "Priority",
JobColumn::Reason => "Reason",
JobColumn::Account => "Account",
JobColumn::Qos => "QOS",
JobColumn::Cpus => "CPUs",
JobColumn::NodeList => "Nodelist",
}
}
pub fn min_width(&self) -> u16 {
match self {
JobColumn::Id => 8,
JobColumn::Name => 10,
JobColumn::Status => 8,
JobColumn::Time => 6,
JobColumn::Partition => 11,
JobColumn::Nodes => 7,
JobColumn::Priority => 10,
JobColumn::Reason => 10,
JobColumn::Account => 9,
JobColumn::Qos => 5,
JobColumn::Cpus => 6,
JobColumn::NodeList => 10,
}
}
pub fn cell(&self, job: &Job) -> String {
match self {
JobColumn::Id => job.id.clone(),
JobColumn::Name => job.name.clone(),
JobColumn::Status => job.status.to_string(),
JobColumn::Time => format_time(job),
JobColumn::Partition => job.partition.clone(),
JobColumn::Nodes => job.nodes.to_string(),
JobColumn::Priority => job.priority.to_string(),
JobColumn::Reason => job.reason.clone().unwrap_or_default(),
JobColumn::Account => job.account.clone(),
JobColumn::Qos => job.qos.clone(),
JobColumn::Cpus => job.cpus.to_string(),
JobColumn::NodeList => job.nodelist.clone(),
}
}
pub fn group_cell(&self, base_id: &str, tasks: &[&Job], expanded: bool) -> String {
match self {
JobColumn::Id => {
let marker = if expanded { "▼" } else { "▶" };
format!("{} {}[]", marker, base_id)
}
JobColumn::Status => status_counts(tasks),
JobColumn::Time => tasks
.iter()
.find(|job| job.status == JobStatus::Running)
.map(|job| format_time(job))
.unwrap_or_else(|| "-".to_string()),
_ => tasks.first().map(|job| self.cell(job)).unwrap_or_default(),
}
}
pub fn from_name(name: &str) -> Option<JobColumn> {
let name = name.trim().to_ascii_lowercase();
ALL_COLUMNS
.into_iter()
.find(|column| column.config_name() == name)
}
pub fn next_in(&self, columns: &[JobColumn]) -> JobColumn {
match columns.iter().position(|column| column == self) {
Some(index) => columns[(index + 1) % columns.len()],
None => *columns.first().unwrap_or(&JobColumn::Id),
}
}
}
pub fn parse_columns(text: &str) -> Option<Vec<JobColumn>> {
let mut columns = Vec::new();
for item in text.split(',') {
if item.trim().is_empty() {
continue;
}
columns.push(JobColumn::from_name(item)?);
}
if columns.is_empty() {
return None;
}
Some(columns)
}
pub fn columns_to_string(columns: &[JobColumn]) -> String {
columns
.iter()
.map(|column| column.config_name())
.collect::<Vec<_>>()
.join(", ")
}
pub fn format_time(job: &Job) -> String {
let time_str = job.time.clone();
let parts: Vec<&str> = time_str.split('-').collect();
match parts.len() {
1 => parts[0].to_string(),
2 => {
let days = parts[0].parse::<i32>().unwrap_or(0);
if days > 0 {
time_str
} else {
parts[1].to_string()
}
}
_ => "".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_columns_match_the_historical_table() {
let defaults = JobColumn::defaults();
assert_eq!(
defaults,
vec![
JobColumn::Id,
JobColumn::Name,
JobColumn::Status,
JobColumn::Time,
JobColumn::Partition,
JobColumn::Nodes,
]
);
let mut category = JobColumn::Id;
let mut cycle = Vec::new();
for _ in 0..6 {
category = category.next_in(&defaults);
cycle.push(category);
}
assert_eq!(
cycle,
vec![
JobColumn::Name,
JobColumn::Status,
JobColumn::Time,
JobColumn::Partition,
JobColumn::Nodes,
JobColumn::Id,
]
);
}
#[test]
fn test_next_in_falls_back_to_first_column() {
let columns = vec![JobColumn::Id, JobColumn::Priority];
assert_eq!(JobColumn::Name.next_in(&columns), JobColumn::Id);
assert_eq!(JobColumn::Name.next_in(&[]), JobColumn::Id);
}
#[test]
fn test_parse_columns() {
assert_eq!(
parse_columns("id,priority"),
Some(vec![JobColumn::Id, JobColumn::Priority])
);
assert_eq!(
parse_columns(" ID , Priority ,, NodeList "),
Some(vec![
JobColumn::Id,
JobColumn::Priority,
JobColumn::NodeList
])
);
assert_eq!(parse_columns("id,bogus"), None);
assert_eq!(parse_columns(""), None);
assert_eq!(parse_columns(" , "), None);
}
#[test]
fn test_columns_round_trip_through_string() {
for column in ALL_COLUMNS {
assert_eq!(JobColumn::from_name(column.config_name()), Some(column));
}
let columns = ALL_COLUMNS.to_vec();
assert_eq!(parse_columns(&columns_to_string(&columns)), Some(columns));
}
#[test]
fn test_cell_extraction() {
let mut job = Job::new_default();
job.priority = 4294901760;
job.account = "physics".to_string();
job.qos = "normal".to_string();
job.cpus = 32;
job.nodelist = "l[42314-42316]".to_string();
job.reason = Some("Priority".to_string());
assert_eq!(JobColumn::Id.cell(&job), "123456");
assert_eq!(JobColumn::Priority.cell(&job), "4294901760");
assert_eq!(JobColumn::Account.cell(&job), "physics");
assert_eq!(JobColumn::Qos.cell(&job), "normal");
assert_eq!(JobColumn::Cpus.cell(&job), "32");
assert_eq!(JobColumn::NodeList.cell(&job), "l[42314-42316]");
assert_eq!(JobColumn::Reason.cell(&job), "Priority");
job.reason = None;
assert_eq!(JobColumn::Reason.cell(&job), "");
}
#[test]
fn test_group_cell() {
let mut running = Job::new_default();
running.id = "12345_3".to_string();
running.name = "array_job".to_string();
running.status = JobStatus::Running;
running.time = "0-01:30:00".to_string();
let mut pending = Job::new_default();
pending.id = "12345_[8-9]".to_string();
pending.status = JobStatus::Pending;
pending.time = "0:00".to_string();
let tasks = vec![&running, &pending];
assert_eq!(
JobColumn::Id.group_cell("12345", &tasks, false),
"▶ 12345[]"
);
assert_eq!(JobColumn::Id.group_cell("12345", &tasks, true), "▼ 12345[]");
assert_eq!(
JobColumn::Status.group_cell("12345", &tasks, false),
"1R 2PD"
);
assert_eq!(
JobColumn::Time.group_cell("12345", &tasks, false),
"01:30:00"
);
assert_eq!(
JobColumn::Name.group_cell("12345", &tasks, false),
"array_job"
);
let tasks = vec![&pending];
assert_eq!(JobColumn::Time.group_cell("12345", &tasks, false), "-");
}
#[test]
fn test_format_time() {
let mut job = Job::new_default();
job.time = "0-00:00:10".to_string();
assert_eq!(format_time(&job), "00:00:10");
job.time = "1-00:00:10".to_string();
assert_eq!(format_time(&job), "1-00:00:10");
}
}