Establish OA/SA prototype baseline and first-release work
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
//! 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. Add relationships from Properties as the model develops.").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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user