merge: panel intake pod rows
This commit is contained in:
commit
2fcbd6aefb
|
|
@ -48,11 +48,11 @@ use crate::workspace_panel::{
|
||||||
ActionPriority, CompanionLifecyclePlan, CompanionPanelState, CompanionPanelStatus,
|
ActionPriority, CompanionLifecyclePlan, CompanionPanelState, CompanionPanelStatus,
|
||||||
CompanionPodPresence, ComposerTarget, NextUserAction, OrchestratorLifecyclePlan,
|
CompanionPodPresence, ComposerTarget, NextUserAction, OrchestratorLifecyclePlan,
|
||||||
OrchestratorPanelState, OrchestratorPanelStatus, OrchestratorPodPresence, PanelRow,
|
OrchestratorPanelState, OrchestratorPanelStatus, OrchestratorPodPresence, PanelRow,
|
||||||
PanelRowKey, TicketConfigAvailability, TicketLocalClaimStatus, WorkspacePanelViewModel,
|
PanelRowKey, PanelRowKind, TicketConfigAvailability, TicketLocalClaimStatus,
|
||||||
bounded_panel_diagnostic, build_current_ticket_row, build_workspace_panel,
|
WorkspacePanelViewModel, bounded_panel_diagnostic, build_current_ticket_row,
|
||||||
companion_pod_presence, decide_companion_lifecycle, decide_orchestrator_lifecycle,
|
build_workspace_panel, companion_pod_presence, decide_companion_lifecycle,
|
||||||
local_claim_status_for_pod, orchestrator_pod_presence, ticket_config_availability,
|
decide_orchestrator_lifecycle, local_claim_status_for_pod, orchestrator_pod_presence,
|
||||||
workspace_companion_pod_name, workspace_orchestrator_pod_name,
|
ticket_config_availability, workspace_companion_pod_name, workspace_orchestrator_pod_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_ENTRIES: usize = 50;
|
const MAX_ENTRIES: usize = 50;
|
||||||
|
|
@ -958,6 +958,13 @@ fn panel_e2e_row_key(key: &PanelRowKey) -> PanelE2eRowKey {
|
||||||
kind: "ticket",
|
kind: "ticket",
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
},
|
},
|
||||||
|
PanelRowKey::TicketIntakePod {
|
||||||
|
ticket_id,
|
||||||
|
pod_name,
|
||||||
|
} => PanelE2eRowKey {
|
||||||
|
kind: "ticket_intake_pod",
|
||||||
|
id: format!("{ticket_id}:{pod_name}"),
|
||||||
|
},
|
||||||
PanelRowKey::Pod(name) => PanelE2eRowKey {
|
PanelRowKey::Pod(name) => PanelE2eRowKey {
|
||||||
kind: "pod",
|
kind: "pod",
|
||||||
id: name.clone(),
|
id: name.clone(),
|
||||||
|
|
@ -1207,12 +1214,8 @@ impl MultiPodApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_pod_entry(&self) -> Option<&PodListEntry> {
|
fn selected_pod_entry(&self) -> Option<&PodListEntry> {
|
||||||
match self.selected_row.as_ref() {
|
let name = self.selected_row.as_ref().and_then(PanelRowKey::pod_name)?;
|
||||||
Some(PanelRowKey::Pod(name)) => {
|
self.list.entries.iter().find(|entry| entry.name == name)
|
||||||
self.list.entries.iter().find(|entry| &entry.name == name)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -1238,11 +1241,14 @@ impl MultiPodApp {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let entry = self.selected_pod_entry()?;
|
if let Some(entry) = self.selected_pod_entry() {
|
||||||
if entry.actions.can_open {
|
if entry.actions.can_open {
|
||||||
return None;
|
return None;
|
||||||
|
}
|
||||||
|
return Some(open_disabled_reason(entry));
|
||||||
}
|
}
|
||||||
Some(open_disabled_reason(entry))
|
self.selected_panel_row()
|
||||||
|
.and_then(|row| row.disabled_reason.clone().or_else(|| row.key_hint.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn select_next(&mut self) {
|
pub(crate) fn select_next(&mut self) {
|
||||||
|
|
@ -1354,6 +1360,9 @@ impl MultiPodApp {
|
||||||
None => match &hit.key {
|
None => match &hit.key {
|
||||||
PanelRowKey::Pod(name) => (name.clone(), None, None),
|
PanelRowKey::Pod(name) => (name.clone(), None, None),
|
||||||
PanelRowKey::Ticket(id) => (id.clone(), None, None),
|
PanelRowKey::Ticket(id) => (id.clone(), None, None),
|
||||||
|
PanelRowKey::TicketIntakePod { pod_name, .. } => {
|
||||||
|
(pod_name.clone(), None, None)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
PanelE2eRenderedRow {
|
PanelE2eRenderedRow {
|
||||||
|
|
@ -1406,7 +1415,7 @@ impl MultiPodApp {
|
||||||
}
|
}
|
||||||
if let Some(key) = visible.iter().find(|key| match key {
|
if let Some(key) = visible.iter().find(|key| match key {
|
||||||
PanelRowKey::Pod(name) => Some(name.as_str()) != orchestrator_pod_name,
|
PanelRowKey::Pod(name) => Some(name.as_str()) != orchestrator_pod_name,
|
||||||
PanelRowKey::Ticket(_) => true,
|
PanelRowKey::Ticket(_) | PanelRowKey::TicketIntakePod { .. } => true,
|
||||||
}) {
|
}) {
|
||||||
self.select_panel_key(key.clone());
|
self.select_panel_key(key.clone());
|
||||||
return;
|
return;
|
||||||
|
|
@ -4677,6 +4686,13 @@ fn selected_ticket_notice(row: Option<&PanelRow>) -> String {
|
||||||
row.title
|
row.title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Some(row) if row.kind == PanelRowKind::TicketIntakePod => row
|
||||||
|
.disabled_reason
|
||||||
|
.clone()
|
||||||
|
.or_else(|| row.key_hint.clone())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
"Open/attach this Ticket's Intake Pod from the associated row.".to_string()
|
||||||
|
}),
|
||||||
_ => "No Pod is selected.".to_string(),
|
_ => "No Pod is selected.".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4793,7 +4809,7 @@ fn visible_panel_keys(panel: &WorkspacePanelViewModel, list: &PodList) -> Vec<Pa
|
||||||
let mut keys = panel
|
let mut keys = panel
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| row.is_ticket_action())
|
.filter(|row| row.is_ticket_section_row())
|
||||||
.map(|row| row.key.clone())
|
.map(|row| row.key.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
keys.extend(
|
keys.extend(
|
||||||
|
|
@ -5145,7 +5161,7 @@ fn panel_action_rows(
|
||||||
let rows = panel
|
let rows = panel
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| row.is_ticket_action())
|
.filter(|row| row.is_ticket_section_row())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
@ -5181,11 +5197,15 @@ fn panel_action_header_line(total: usize, width: u16) -> Line<'static> {
|
||||||
const TICKET_STATE_COLUMN_WIDTH: usize = 10;
|
const TICKET_STATE_COLUMN_WIDTH: usize = 10;
|
||||||
const POD_STATUS_COLUMN_WIDTH: usize = 18;
|
const POD_STATUS_COLUMN_WIDTH: usize = 18;
|
||||||
|
|
||||||
fn panel_row_lines(row: &PanelRow, selected: bool, width: u16) -> [Line<'static>; 2] {
|
fn panel_row_lines(row: &PanelRow, selected: bool, width: u16) -> Vec<Line<'static>> {
|
||||||
[
|
if row.kind == PanelRowKind::TicketIntakePod {
|
||||||
panel_row_title_line(row, selected, width),
|
vec![panel_row_title_line(row, selected, width)]
|
||||||
panel_row_detail_line(row, selected, width),
|
} else {
|
||||||
]
|
vec![
|
||||||
|
panel_row_title_line(row, selected, width),
|
||||||
|
panel_row_detail_line(row, selected, width),
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn panel_row_title_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> {
|
fn panel_row_title_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> {
|
||||||
|
|
@ -5242,6 +5262,21 @@ fn push_ticket_marker_span(spans: &mut Vec<Span<'static>>, selected: bool, remai
|
||||||
}
|
}
|
||||||
|
|
||||||
fn panel_ticket_detail(row: &PanelRow) -> String {
|
fn panel_ticket_detail(row: &PanelRow) -> String {
|
||||||
|
if row.kind == PanelRowKind::TicketIntakePod {
|
||||||
|
let mut parts = row
|
||||||
|
.subtitle
|
||||||
|
.as_ref()
|
||||||
|
.map(|subtitle| vec![subtitle.clone()])
|
||||||
|
.unwrap_or_else(|| vec![panel_ticket_reference(row)]);
|
||||||
|
if let Some(action) = row.next_action {
|
||||||
|
parts.push(format!("Action: {}", action.label()));
|
||||||
|
}
|
||||||
|
if let Some(reason) = panel_ticket_reason(row) {
|
||||||
|
parts.push(format!("Reason: {reason}"));
|
||||||
|
}
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
let mut parts = vec![panel_ticket_reference(row)];
|
let mut parts = vec![panel_ticket_reference(row)];
|
||||||
if let Some(blocked_reason) = row
|
if let Some(blocked_reason) = row
|
||||||
.ticket
|
.ticket
|
||||||
|
|
@ -5303,6 +5338,7 @@ fn panel_ticket_reference(row: &PanelRow) -> String {
|
||||||
.map(|ticket| ticket.id.clone())
|
.map(|ticket| ticket.id.clone())
|
||||||
.unwrap_or_else(|| match &row.key {
|
.unwrap_or_else(|| match &row.key {
|
||||||
PanelRowKey::Ticket(id) => id.clone(),
|
PanelRowKey::Ticket(id) => id.clone(),
|
||||||
|
PanelRowKey::TicketIntakePod { ticket_id, .. } => ticket_id.clone(),
|
||||||
PanelRowKey::Pod(name) => name.clone(),
|
PanelRowKey::Pod(name) => name.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -7321,7 +7357,8 @@ branch = "orchestration/custom-panel"
|
||||||
"inprogress",
|
"inprogress",
|
||||||
);
|
);
|
||||||
|
|
||||||
let [title, detail] = panel_row_lines(&row, true, 160);
|
let lines = panel_row_lines(&row, true, 160);
|
||||||
|
let (title, detail) = (&lines[0], &lines[1]);
|
||||||
let title_line = plain_line(&title);
|
let title_line = plain_line(&title);
|
||||||
let detail_line = plain_line(&detail);
|
let detail_line = plain_line(&detail);
|
||||||
let state_start = 2;
|
let state_start = 2;
|
||||||
|
|
@ -7352,7 +7389,8 @@ branch = "orchestration/custom-panel"
|
||||||
"ready",
|
"ready",
|
||||||
);
|
);
|
||||||
|
|
||||||
let [title, detail] = panel_row_lines(&row, false, 160);
|
let lines = panel_row_lines(&row, false, 160);
|
||||||
|
let (title, detail) = (&lines[0], &lines[1]);
|
||||||
let title_line = plain_line(&title);
|
let title_line = plain_line(&title);
|
||||||
let detail_line = plain_line(&detail);
|
let detail_line = plain_line(&detail);
|
||||||
let state_start = 2;
|
let state_start = 2;
|
||||||
|
|
@ -7377,7 +7415,8 @@ branch = "orchestration/custom-panel"
|
||||||
"ready",
|
"ready",
|
||||||
);
|
);
|
||||||
|
|
||||||
let [title, detail] = panel_row_lines(&row, false, 42);
|
let lines = panel_row_lines(&row, false, 42);
|
||||||
|
let (title, detail) = (&lines[0], &lines[1]);
|
||||||
let title_line = plain_line(&title);
|
let title_line = plain_line(&title);
|
||||||
let detail_line = plain_line(&detail);
|
let detail_line = plain_line(&detail);
|
||||||
let title_start = 2 + TICKET_STATE_COLUMN_WIDTH + 1;
|
let title_start = 2 + TICKET_STATE_COLUMN_WIDTH + 1;
|
||||||
|
|
@ -7402,7 +7441,8 @@ branch = "orchestration/custom-panel"
|
||||||
row.disabled_reason = Some("Queue disabled: waiting for BLOCKER-1".to_string());
|
row.disabled_reason = Some("Queue disabled: waiting for BLOCKER-1".to_string());
|
||||||
row.ticket.as_mut().unwrap().blocked_reason = Some("BLOCKER-1 via depends_on".to_string());
|
row.ticket.as_mut().unwrap().blocked_reason = Some("BLOCKER-1 via depends_on".to_string());
|
||||||
|
|
||||||
let [_title, detail] = panel_row_lines(&row, true, 160);
|
let lines = panel_row_lines(&row, true, 160);
|
||||||
|
let detail = &lines[1];
|
||||||
let detail_line = plain_line(&detail);
|
let detail_line = plain_line(&detail);
|
||||||
|
|
||||||
assert!(detail_line.contains("Gate: waiting for BLOCKER-1 via depends_on"));
|
assert!(detail_line.contains("Gate: waiting for BLOCKER-1 via depends_on"));
|
||||||
|
|
@ -8602,6 +8642,7 @@ branch = "orchestration/custom-panel"
|
||||||
blocked_reason: None,
|
blocked_reason: None,
|
||||||
related_pods: Vec::new(),
|
related_pods: Vec::new(),
|
||||||
local_claim: None,
|
local_claim: None,
|
||||||
|
intake_pods: Vec::new(),
|
||||||
};
|
};
|
||||||
PanelRow {
|
PanelRow {
|
||||||
key: PanelRowKey::Ticket(ticket.id.clone()),
|
key: PanelRowKey::Ticket(ticket.id.clone()),
|
||||||
|
|
|
||||||
|
|
@ -182,15 +182,26 @@ impl OrchestratorPanelStatus {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub(crate) enum PanelRowKey {
|
pub(crate) enum PanelRowKey {
|
||||||
Ticket(String),
|
Ticket(String),
|
||||||
|
TicketIntakePod { ticket_id: String, pod_name: String },
|
||||||
Pod(String),
|
Pod(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PanelRowKey {
|
||||||
|
pub(crate) fn pod_name(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Self::Pod(name) | Self::TicketIntakePod { pod_name: name, .. } => Some(name),
|
||||||
|
Self::Ticket(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum PanelRowKind {
|
pub(crate) enum PanelRowKind {
|
||||||
Planning,
|
Planning,
|
||||||
Ticket,
|
Ticket,
|
||||||
Review,
|
Review,
|
||||||
ActiveWork,
|
ActiveWork,
|
||||||
|
TicketIntakePod,
|
||||||
Pod,
|
Pod,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +247,30 @@ pub(crate) struct TicketPanelEntry {
|
||||||
pub(crate) blocked_reason: Option<String>,
|
pub(crate) blocked_reason: Option<String>,
|
||||||
pub(crate) related_pods: Vec<String>,
|
pub(crate) related_pods: Vec<String>,
|
||||||
pub(crate) local_claim: Option<TicketLocalClaimEntry>,
|
pub(crate) local_claim: Option<TicketLocalClaimEntry>,
|
||||||
|
pub(crate) intake_pods: Vec<TicketAssociatedIntakeEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct TicketAssociatedIntakeEntry {
|
||||||
|
pub(crate) ticket_id: String,
|
||||||
|
pub(crate) pod_name: String,
|
||||||
|
pub(crate) status: TicketLocalClaimStatus,
|
||||||
|
pub(crate) source: TicketAssociatedIntakeSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum TicketAssociatedIntakeSource {
|
||||||
|
LocalClaim,
|
||||||
|
RelatedSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketAssociatedIntakeSource {
|
||||||
|
pub(crate) fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::LocalClaim => "local claim",
|
||||||
|
Self::RelatedSession => "related session",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -279,11 +314,22 @@ pub(crate) struct PanelRow {
|
||||||
|
|
||||||
impl PanelRow {
|
impl PanelRow {
|
||||||
pub(crate) fn is_ticket_action(&self) -> bool {
|
pub(crate) fn is_ticket_action(&self) -> bool {
|
||||||
!matches!(self.kind, PanelRowKind::Pod)
|
matches!(
|
||||||
|
self.kind,
|
||||||
|
PanelRowKind::Planning
|
||||||
|
| PanelRowKind::Ticket
|
||||||
|
| PanelRowKind::Review
|
||||||
|
| PanelRowKind::ActiveWork
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_ticket_section_row(&self) -> bool {
|
||||||
|
self.is_ticket_action() || matches!(self.kind, PanelRowKind::TicketIntakePod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_POD_NAME_CHARS: usize = 80;
|
const MAX_POD_NAME_CHARS: usize = 80;
|
||||||
|
const MAX_ASSOCIATED_INTAKE_ROWS_PER_TICKET: usize = 3;
|
||||||
const ORCHESTRATOR_SUFFIX: &str = "-orchestrator";
|
const ORCHESTRATOR_SUFFIX: &str = "-orchestrator";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -574,12 +620,6 @@ fn build_workspace_panel_with_registry_model(
|
||||||
}
|
}
|
||||||
|
|
||||||
model.rows.extend(pod_rows(pods));
|
model.rows.extend(pod_rows(pods));
|
||||||
model.rows.sort_by(|a, b| {
|
|
||||||
a.priority
|
|
||||||
.cmp(&b.priority)
|
|
||||||
.then_with(|| row_updated_at(b).cmp(row_updated_at(a)))
|
|
||||||
.then_with(|| a.title.cmp(&b.title))
|
|
||||||
});
|
|
||||||
model
|
model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,13 +668,13 @@ fn build_ticket_rows(
|
||||||
pods: &PodList,
|
pods: &PodList,
|
||||||
registry: &PanelRegistrySnapshot,
|
registry: &PanelRegistrySnapshot,
|
||||||
) -> ticket::Result<Vec<PanelRow>> {
|
) -> ticket::Result<Vec<PanelRow>> {
|
||||||
let mut rows = Vec::new();
|
let mut ticket_rows = Vec::new();
|
||||||
for summary in backend.list(TicketFilter::all())? {
|
for summary in backend.list(TicketFilter::all())? {
|
||||||
if summary.workflow_state == TicketWorkflowState::Closed {
|
if summary.workflow_state == TicketWorkflowState::Closed {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ticket = backend.show(TicketIdOrSlug::Query(summary.id.clone()))?;
|
let ticket = backend.show(TicketIdOrSlug::Query(summary.id.clone()))?;
|
||||||
rows.push(ticket_row(
|
ticket_rows.push(ticket_row(
|
||||||
summary,
|
summary,
|
||||||
&ticket.events,
|
&ticket.events,
|
||||||
&ticket.relations.blockers,
|
&ticket.relations.blockers,
|
||||||
|
|
@ -642,6 +682,19 @@ fn build_ticket_rows(
|
||||||
registry,
|
registry,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
ticket_rows.sort_by(|a, b| {
|
||||||
|
a.priority
|
||||||
|
.cmp(&b.priority)
|
||||||
|
.then_with(|| row_updated_at(b).cmp(row_updated_at(a)))
|
||||||
|
.then_with(|| a.title.cmp(&b.title))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for row in ticket_rows {
|
||||||
|
let intake_rows = ticket_intake_pod_rows(&row);
|
||||||
|
rows.push(row);
|
||||||
|
rows.extend(intake_rows);
|
||||||
|
}
|
||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -653,7 +706,17 @@ fn ticket_row(
|
||||||
registry: &PanelRegistrySnapshot,
|
registry: &PanelRegistrySnapshot,
|
||||||
) -> PanelRow {
|
) -> PanelRow {
|
||||||
let local_claim = local_claim_for_ticket(&summary, pods, registry);
|
let local_claim = local_claim_for_ticket(&summary, pods, registry);
|
||||||
let related_pods = related_pods_for_ticket(&summary, pods, registry);
|
let intake_pods =
|
||||||
|
associated_intake_entries_for_ticket(&summary, pods, registry, local_claim.as_ref());
|
||||||
|
let mut related_pods = Vec::new();
|
||||||
|
if let Some(claim) = local_claim.as_ref() {
|
||||||
|
related_pods.push(claim.pod_name.clone());
|
||||||
|
}
|
||||||
|
for pod_name in intake_pods.iter().map(|intake| intake.pod_name.clone()) {
|
||||||
|
if !related_pods.iter().any(|existing| existing == &pod_name) {
|
||||||
|
related_pods.push(pod_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
let derived = derive_ticket_state(&summary, relation_blockers);
|
let derived = derive_ticket_state(&summary, relation_blockers);
|
||||||
let latest_event = events.last();
|
let latest_event = events.last();
|
||||||
let entry = TicketPanelEntry {
|
let entry = TicketPanelEntry {
|
||||||
|
|
@ -669,6 +732,7 @@ fn ticket_row(
|
||||||
blocked_reason: derived.blocked_reason.clone(),
|
blocked_reason: derived.blocked_reason.clone(),
|
||||||
related_pods: related_pods.clone(),
|
related_pods: related_pods.clone(),
|
||||||
local_claim,
|
local_claim,
|
||||||
|
intake_pods,
|
||||||
};
|
};
|
||||||
let subtitle = ticket_subtitle(&entry);
|
let subtitle = ticket_subtitle(&entry);
|
||||||
PanelRow {
|
PanelRow {
|
||||||
|
|
@ -802,32 +866,111 @@ fn derive_ticket_state(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn related_pods_for_ticket(
|
fn associated_intake_entries_for_ticket(
|
||||||
summary: &TicketSummary,
|
summary: &TicketSummary,
|
||||||
pods: &PodList,
|
pods: &PodList,
|
||||||
registry: &PanelRegistrySnapshot,
|
registry: &PanelRegistrySnapshot,
|
||||||
) -> Vec<String> {
|
local_claim: Option<&TicketLocalClaimEntry>,
|
||||||
let id = lowercase(&summary.id);
|
) -> Vec<TicketAssociatedIntakeEntry> {
|
||||||
let mut names = Vec::new();
|
let mut entries = Vec::new();
|
||||||
if let Some(claim) = registry.claim_for_ticket(&summary.id) {
|
if let Some(claim) = local_claim.filter(|claim| is_intake_role(&claim.role)) {
|
||||||
names.push(claim.pod_name.clone());
|
entries.push(TicketAssociatedIntakeEntry {
|
||||||
|
ticket_id: summary.id.clone(),
|
||||||
|
pod_name: claim.pod_name.clone(),
|
||||||
|
status: claim.status,
|
||||||
|
source: TicketAssociatedIntakeSource::LocalClaim,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for pod in pods.entries.iter().filter_map(|pod| {
|
|
||||||
let name = lowercase(&pod.name);
|
let mut related_sessions = registry
|
||||||
if !id.is_empty() && name.contains(&id) {
|
.sessions
|
||||||
Some(pod.name.clone())
|
.iter()
|
||||||
} else {
|
.filter(|session| {
|
||||||
None
|
is_intake_role(&session.role)
|
||||||
|
&& session
|
||||||
|
.related_tickets
|
||||||
|
.iter()
|
||||||
|
.any(|related| related.id == summary.id.as_str())
|
||||||
|
})
|
||||||
|
.map(|session| session.pod_name.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
related_sessions.sort();
|
||||||
|
related_sessions.dedup();
|
||||||
|
|
||||||
|
for pod_name in related_sessions {
|
||||||
|
if entries.iter().any(|entry| entry.pod_name == pod_name) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}) {
|
entries.push(TicketAssociatedIntakeEntry {
|
||||||
if !names.iter().any(|existing| existing == &pod) {
|
ticket_id: summary.id.clone(),
|
||||||
names.push(pod);
|
status: local_claim_status_for_pod(&pod_name, pods),
|
||||||
}
|
pod_name,
|
||||||
if names.len() >= 5 {
|
source: TicketAssociatedIntakeSource::RelatedSession,
|
||||||
|
});
|
||||||
|
if entries.len() >= MAX_ASSOCIATED_INTAKE_ROWS_PER_TICKET {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
names
|
|
||||||
|
entries.truncate(MAX_ASSOCIATED_INTAKE_ROWS_PER_TICKET);
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_intake_role(role: &str) -> bool {
|
||||||
|
role.eq_ignore_ascii_case("intake")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ticket_intake_pod_rows(row: &PanelRow) -> Vec<PanelRow> {
|
||||||
|
row.ticket
|
||||||
|
.as_ref()
|
||||||
|
.map(|ticket| {
|
||||||
|
ticket
|
||||||
|
.intake_pods
|
||||||
|
.iter()
|
||||||
|
.map(ticket_intake_pod_row)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ticket_intake_pod_row(intake: &TicketAssociatedIntakeEntry) -> PanelRow {
|
||||||
|
let stale = intake.status == TicketLocalClaimStatus::Stale;
|
||||||
|
PanelRow {
|
||||||
|
key: PanelRowKey::TicketIntakePod {
|
||||||
|
ticket_id: intake.ticket_id.clone(),
|
||||||
|
pod_name: intake.pod_name.clone(),
|
||||||
|
},
|
||||||
|
kind: PanelRowKind::TicketIntakePod,
|
||||||
|
title: format!("↳ Intake Pod: {}", intake.pod_name),
|
||||||
|
subtitle: Some(format!(
|
||||||
|
"Ticket {} · {} · {}",
|
||||||
|
intake.ticket_id,
|
||||||
|
intake.source.label(),
|
||||||
|
intake.status.label()
|
||||||
|
)),
|
||||||
|
status: intake.status.label().to_string(),
|
||||||
|
priority: ActionPriority::ActiveWork,
|
||||||
|
next_action: if stale {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(NextUserAction::OpenPod)
|
||||||
|
},
|
||||||
|
ticket: None,
|
||||||
|
related_pods: vec![intake.pod_name.clone()],
|
||||||
|
disabled_reason: if stale {
|
||||||
|
Some(
|
||||||
|
"Associated Intake Pod is stale; no live or restorable Pod entry is available."
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
key_hint: Some(if stale {
|
||||||
|
"Stale Intake claim/session; restore is unavailable".to_string()
|
||||||
|
} else {
|
||||||
|
"Open/attach this Ticket's Intake Pod".to_string()
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_claim_for_ticket(
|
fn local_claim_for_ticket(
|
||||||
|
|
@ -962,15 +1105,12 @@ fn excerpt(markdown: &str, max_chars: usize) -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lowercase(value: &str) -> String {
|
|
||||||
value.to_ascii_lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::pod_list::{LivePodInfo, PodEntrySummary};
|
use crate::pod_list::{LivePodInfo, PodEntrySummary};
|
||||||
|
use crate::role_session_registry::{PanelRegistryStore, RelatedTicketRef, RoleSessionOrigin};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
@ -1196,6 +1336,97 @@ mod tests {
|
||||||
assert_eq!(done.next_action, Some(NextUserAction::Close));
|
assert_eq!(done.next_action, Some(NextUserAction::Close));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_panel_shows_ticket_associated_intake_pods_adjacent_to_ticket() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
write_ticket_config(temp.path());
|
||||||
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
|
create_ticket(&backend, "Ticket With Intake", |input| {
|
||||||
|
input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||||
|
});
|
||||||
|
let ticket_id = backend
|
||||||
|
.list(TicketFilter::all())
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.find(|ticket| ticket.title == "Ticket With Intake")
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let preticket_pod = format!("pre-{ticket_id}-intake");
|
||||||
|
let registry = PanelRegistryStore::from_root(temp.path().join("registry"));
|
||||||
|
registry
|
||||||
|
.claim_ticket(&ticket_id, None, "claimed-intake", "intake")
|
||||||
|
.unwrap();
|
||||||
|
registry
|
||||||
|
.record_session(
|
||||||
|
"shared-intake",
|
||||||
|
"intake",
|
||||||
|
RoleSessionOrigin::RoleLaunch,
|
||||||
|
None,
|
||||||
|
[RelatedTicketRef {
|
||||||
|
id: ticket_id.clone(),
|
||||||
|
slug: None,
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
registry
|
||||||
|
.record_session(
|
||||||
|
&preticket_pod,
|
||||||
|
"intake",
|
||||||
|
RoleSessionOrigin::PreTicketIntake,
|
||||||
|
None,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pods = live_pods(&["claimed-intake", "shared-intake", &preticket_pod]);
|
||||||
|
let model =
|
||||||
|
build_workspace_panel_with_registry(temp.path(), &pods, ®istry.snapshot().unwrap());
|
||||||
|
|
||||||
|
let ticket_index = model
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.position(|row| row.key == PanelRowKey::Ticket(ticket_id.clone()))
|
||||||
|
.unwrap();
|
||||||
|
let ticket_row = &model.rows[ticket_index];
|
||||||
|
let ticket = ticket_row.ticket.as_ref().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
ticket
|
||||||
|
.intake_pods
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.pod_name.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["claimed-intake", "shared-intake"]
|
||||||
|
);
|
||||||
|
assert_eq!(ticket.related_pods, vec!["claimed-intake", "shared-intake"]);
|
||||||
|
assert_eq!(
|
||||||
|
model.rows[ticket_index + 1].key,
|
||||||
|
PanelRowKey::TicketIntakePod {
|
||||||
|
ticket_id: ticket_id.clone(),
|
||||||
|
pod_name: "claimed-intake".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.rows[ticket_index + 1].kind,
|
||||||
|
PanelRowKind::TicketIntakePod
|
||||||
|
);
|
||||||
|
assert_eq!(model.rows[ticket_index + 1].status, "live");
|
||||||
|
assert_eq!(
|
||||||
|
model.rows[ticket_index + 1].next_action,
|
||||||
|
Some(NextUserAction::OpenPod)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
model.rows[ticket_index + 2].key,
|
||||||
|
PanelRowKey::TicketIntakePod {
|
||||||
|
ticket_id: ticket_id.clone(),
|
||||||
|
pod_name: "shared-intake".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(model.rows.iter().all(|row| {
|
||||||
|
row.kind != PanelRowKind::TicketIntakePod
|
||||||
|
|| row.key.pod_name() != Some(preticket_pod.as_str())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workspace_panel_displays_local_ticket_claim_status() {
|
fn workspace_panel_displays_local_ticket_claim_status() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user