294 lines
8.6 KiB
Rust
294 lines
8.6 KiB
Rust
//! Read-only semantic navigation derived from the core's reference visitors.
|
|
use crate::{
|
|
app::Message,
|
|
style,
|
|
views::{action, label, short},
|
|
};
|
|
use iced::{
|
|
Element, Fill,
|
|
widget::{column, container, text},
|
|
};
|
|
use oa_core::{
|
|
references::{element_references, relation_references},
|
|
*,
|
|
};
|
|
use std::collections::BTreeSet;
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub enum InspectorTab {
|
|
#[default]
|
|
Properties,
|
|
Connections,
|
|
}
|
|
|
|
struct Group {
|
|
title: &'static str,
|
|
ids: BTreeSet<Id>,
|
|
}
|
|
|
|
fn groups(p: &Project, selected: Id) -> Vec<Group> {
|
|
let parent = p.elements.get(&selected).and_then(|e| e.parent);
|
|
let children = p.children(selected).map(|e| e.id).collect::<BTreeSet<_>>();
|
|
let mut direct = p
|
|
.elements
|
|
.get(&selected)
|
|
.map(element_references)
|
|
.or_else(|| p.relations.get(&selected).map(relation_references))
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.collect::<BTreeSet<_>>();
|
|
direct.retain(|id| Some(*id) != parent && !children.contains(id));
|
|
let outgoing = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.source() == selected)
|
|
.map(|r| r.id)
|
|
.collect();
|
|
let incoming = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.target() == selected)
|
|
.map(|r| r.id)
|
|
.collect();
|
|
let users = p
|
|
.elements
|
|
.values()
|
|
.filter(|e| {
|
|
e.id != selected
|
|
&& Some(e.id) != parent
|
|
&& e.parent != Some(selected)
|
|
&& element_references(e).contains(&selected)
|
|
})
|
|
.map(|e| e.id)
|
|
.chain(
|
|
p.relations
|
|
.values()
|
|
.filter(|r| {
|
|
r.source() != selected
|
|
&& r.target() != selected
|
|
&& relation_references(r).contains(&selected)
|
|
})
|
|
.map(|r| r.id),
|
|
)
|
|
.collect();
|
|
let counterparts = p
|
|
.traces
|
|
.values()
|
|
.filter_map(|t| {
|
|
if t.oa == selected {
|
|
Some(t.sa)
|
|
} else if t.sa == selected {
|
|
Some(t.oa)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
let artifacts = p
|
|
.artifact_links
|
|
.values()
|
|
.filter(|a| a.owner == selected)
|
|
.map(|a| a.id)
|
|
.collect();
|
|
vec![
|
|
Group {
|
|
title: "OWNED BY",
|
|
ids: parent.into_iter().collect(),
|
|
},
|
|
Group {
|
|
title: "CONTAINS",
|
|
ids: children,
|
|
},
|
|
Group {
|
|
title: "REFERENCES",
|
|
ids: direct,
|
|
},
|
|
Group {
|
|
title: "OUTGOING RELATIONSHIPS",
|
|
ids: outgoing,
|
|
},
|
|
Group {
|
|
title: "INCOMING RELATIONSHIPS",
|
|
ids: incoming,
|
|
},
|
|
Group {
|
|
title: "USED BY BEHAVIOR & CONTRACTS",
|
|
ids: users,
|
|
},
|
|
Group {
|
|
title: "COUNTERPARTS",
|
|
ids: counterparts,
|
|
},
|
|
Group {
|
|
title: "ENGINEERING ARTIFACTS",
|
|
ids: artifacts,
|
|
},
|
|
]
|
|
}
|
|
|
|
pub fn diagrams_containing(p: &Project, selected: Id) -> Vec<Id> {
|
|
let endpoints = p.relations.get(&selected).map(|r| [r.source(), r.target()]);
|
|
p.diagrams
|
|
.values()
|
|
.filter(|d| {
|
|
let placed = |id| {
|
|
let e = p.elements.get(&id);
|
|
let owner = e
|
|
.filter(|e| e.kind() == Kind::Port)
|
|
.and_then(|e| e.parent)
|
|
.unwrap_or(id);
|
|
let kind = p.elements.get(&owner).map(|e| e.kind());
|
|
kind.is_none_or(|kind| d.projection.includes(kind))
|
|
&& d.nodes.values().any(|n| n.element == owner || n.id == id)
|
|
};
|
|
placed(selected) || endpoints.is_some_and(|ends| ends.into_iter().all(placed))
|
|
})
|
|
.map(|d| d.id)
|
|
.collect()
|
|
}
|
|
|
|
pub fn view(p: &Project, selected: Id) -> Element<'_, Message> {
|
|
let mut content = column![
|
|
text(short(p.named(selected), 50)).size(18),
|
|
text("Follow ownership, usage and engineering relationships.")
|
|
.size(12)
|
|
.color(style::MUTED)
|
|
]
|
|
.spacing(12);
|
|
let mut count = 0;
|
|
for group in groups(p, selected) {
|
|
if group.ids.is_empty() {
|
|
continue;
|
|
}
|
|
count += group.ids.len();
|
|
let mut entries = group.ids.into_iter().collect::<Vec<_>>();
|
|
entries.sort_by_key(|id| p.named(*id).to_lowercase());
|
|
content = content.push(label(group.title));
|
|
for id in entries {
|
|
let detail = p
|
|
.elements
|
|
.get(&id)
|
|
.map(|e| format!("{} · {}", e.kind(), e.layer()))
|
|
.or_else(|| {
|
|
p.relations.get(&id).map(|r| {
|
|
format!(
|
|
"{} · {} → {}",
|
|
r.kind(),
|
|
p.named(r.source()),
|
|
p.named(r.target())
|
|
)
|
|
})
|
|
})
|
|
.or_else(|| p.artifact_links.get(&id).map(|a| a.uri.clone()))
|
|
.unwrap_or_default();
|
|
let mut entry = column![
|
|
action(&short(p.named(id), 42), Message::Select(id)).width(Fill),
|
|
text(detail).size(11).color(style::MUTED)
|
|
]
|
|
.spacing(3);
|
|
if let Some(r) = p.relations.get(&id) {
|
|
let other = if r.source() == selected {
|
|
r.target()
|
|
} else {
|
|
r.source()
|
|
};
|
|
entry = entry.push(
|
|
action(
|
|
&format!("Open {}", short(p.named(other), 32)),
|
|
Message::Select(other),
|
|
)
|
|
.width(Fill),
|
|
);
|
|
}
|
|
content = content.push(container(entry).padding(8).style(style::subtle));
|
|
}
|
|
}
|
|
let diagrams = diagrams_containing(p, selected);
|
|
if !diagrams.is_empty() {
|
|
content = content.push(label("SHOWN IN DIAGRAMS"));
|
|
for id in diagrams {
|
|
content = content.push(
|
|
action(
|
|
&short(p.named(id), 42),
|
|
Message::RevealInDiagram(id, selected),
|
|
)
|
|
.width(Fill),
|
|
);
|
|
}
|
|
} else if p.elements.contains_key(&selected) {
|
|
content = content.push(
|
|
text("This element has no diagram placement yet.")
|
|
.size(12)
|
|
.color(style::MUTED),
|
|
);
|
|
}
|
|
if count == 0 {
|
|
content = content.push(text("No semantic connections yet. Use Actions or right-click an element to add relationships.").size(12).color(style::MUTED));
|
|
}
|
|
content.into()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn semantic_browser_finds_behavior_users_without_confusing_containment() {
|
|
let mut p = Project::empty();
|
|
let work = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
|
|
.unwrap();
|
|
let process = p
|
|
.add_element(Kind::Process, Layer::Oa, "Response route", None)
|
|
.unwrap();
|
|
let occurrence = p
|
|
.add_element(Kind::Occurrence, Layer::Oa, "Respond here", Some(process))
|
|
.unwrap();
|
|
*p.elements
|
|
.get_mut(&occurrence)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: None,
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let result = groups(&p, work);
|
|
assert!(
|
|
result
|
|
.iter()
|
|
.find(|g| g.title == "USED BY BEHAVIOR & CONTRACTS")
|
|
.unwrap()
|
|
.ids
|
|
.contains(&occurrence)
|
|
);
|
|
let result = groups(&p, occurrence);
|
|
assert!(
|
|
result
|
|
.iter()
|
|
.find(|g| g.title == "OWNED BY")
|
|
.unwrap()
|
|
.ids
|
|
.contains(&process)
|
|
);
|
|
assert!(
|
|
result
|
|
.iter()
|
|
.find(|g| g.title == "REFERENCES")
|
|
.unwrap()
|
|
.ids
|
|
.contains(&work)
|
|
);
|
|
assert!(
|
|
!result
|
|
.iter()
|
|
.find(|g| g.title == "REFERENCES")
|
|
.unwrap()
|
|
.ids
|
|
.contains(&process)
|
|
);
|
|
}
|
|
}
|