coil_admin/model/
shell.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct AdminShell {
5 accessibility: AccessibilityContract,
6 resources: Vec<AdminResourceDescriptor>,
7 widgets: Vec<AdminWidgetDescriptor>,
8 workflows: Vec<WorkflowAction>,
9 audit_log: Vec<AuditEntry>,
10}
11
12impl AdminShell {
13 pub fn new(
14 accessibility: AccessibilityContract,
15 resources: Vec<AdminResourceDescriptor>,
16 widgets: Vec<AdminWidgetDescriptor>,
17 workflows: Vec<WorkflowAction>,
18 ) -> Result<Self, AdminModelError> {
19 ensure_unique_resources(&resources)?;
20 ensure_unique_widgets(&widgets)?;
21 ensure_unique_workflows(&workflows)?;
22 Ok(Self {
23 accessibility,
24 resources,
25 widgets,
26 workflows,
27 audit_log: Vec::new(),
28 })
29 }
30
31 pub fn accessibility(&self) -> &AccessibilityContract {
32 &self.accessibility
33 }
34
35 pub fn resources(&self) -> &[AdminResourceDescriptor] {
36 &self.resources
37 }
38
39 pub fn widgets(&self) -> &[AdminWidgetDescriptor] {
40 &self.widgets
41 }
42
43 pub fn workflows(&self) -> &[WorkflowAction] {
44 &self.workflows
45 }
46
47 pub fn visible_resources(
48 &self,
49 operator: &OperatorAccessContext,
50 ) -> Vec<AdminResourceDescriptor> {
51 self.resources
52 .iter()
53 .filter(|resource| operator.allows(resource.required_capability))
54 .cloned()
55 .collect()
56 }
57
58 pub fn compose_module_resources(
59 manifests: &[ModuleManifest],
60 ) -> Result<Vec<AdminResourceDescriptor>, AdminModelError> {
61 let mut resources = Vec::new();
62 for manifest in manifests {
63 for contribution in &manifest.admin_resources {
64 resources.push(AdminResourceDescriptor::from_contribution(contribution)?);
65 }
66 }
67 ensure_unique_resources(&resources)?;
68 Ok(resources)
69 }
70
71 pub fn compose_module_workflows(
72 manifests: &[ModuleManifest],
73 ) -> Result<Vec<WorkflowAction>, AdminModelError> {
74 let mut workflows = Vec::new();
75 for manifest in manifests {
76 for definition in &manifest.bulk_operations {
77 workflows.push(WorkflowAction::from_bulk_operation(definition)?);
78 }
79 }
80 ensure_unique_workflows(&workflows)?;
81 Ok(workflows)
82 }
83
84 pub fn compose_extension_widgets(
85 registry: &ExtensionRegistry,
86 ) -> Result<Vec<AdminWidgetDescriptor>, AdminModelError> {
87 let mut widgets = Vec::new();
88
89 for handler in registry.registered_handlers() {
90 if handler.point != coil_wasm::ExtensionPointKind::AdminWidget {
91 continue;
92 }
93
94 widgets.push(AdminWidgetDescriptor::new(
95 AdminWidgetId::new(format!(
96 "ext.{}.{}",
97 handler.extension_id, handler.handler_id
98 ))?,
99 format!("{} widget", handler.extension_id),
100 map_extension_widget_slot(&handler.surface),
101 Some(Capability::AdminShellAccess),
102 None,
103 )?);
104 }
105
106 ensure_unique_widgets(&widgets)?;
107 Ok(widgets)
108 }
109
110 pub fn navigation_by_section(
111 &self,
112 operator: &OperatorAccessContext,
113 ) -> HashMap<NavigationSection, Vec<AdminResourceDescriptor>> {
114 let mut grouped = HashMap::new();
115 for resource in self.visible_resources(operator) {
116 grouped
117 .entry(resource.section)
118 .or_insert_with(Vec::new)
119 .push(resource);
120 }
121 grouped
122 }
123
124 pub fn visible_widgets(&self, operator: &OperatorAccessContext) -> Vec<AdminWidgetDescriptor> {
125 self.widgets
126 .iter()
127 .filter(|widget| {
128 widget
129 .required_capability
130 .is_none_or(|capability| operator.allows(capability))
131 })
132 .cloned()
133 .collect()
134 }
135
136 pub fn build_bulk_action_plan(
137 &self,
138 workflow_id: &WorkflowId,
139 resource_count: usize,
140 operator: &OperatorAccessContext,
141 ) -> Option<BulkActionPlan> {
142 let workflow = self
143 .workflows
144 .iter()
145 .find(|workflow| &workflow.id == workflow_id)?;
146 if !operator.allows(workflow.required_capability) {
147 return None;
148 }
149
150 Some(BulkActionPlan {
151 workflow_id: workflow.id.clone(),
152 resource_count,
153 message: workflow.success_message.clone(),
154 })
155 }
156
157 pub fn record_audit_entry(&mut self, entry: AuditEntry) {
158 self.audit_log.push(entry);
159 }
160
161 pub fn visible_audit_entries(&self, operator: &OperatorAccessContext) -> &[AuditEntry] {
162 if operator.allows(Capability::AdminAuditRead) {
163 &self.audit_log
164 } else {
165 &[]
166 }
167 }
168}
169
170fn ensure_unique_resources(resources: &[AdminResourceDescriptor]) -> Result<(), AdminModelError> {
171 let mut seen = HashSet::new();
172 for resource in resources {
173 if !seen.insert(resource.id.clone()) {
174 return Err(AdminModelError::DuplicateResource {
175 resource_id: resource.id.to_string(),
176 });
177 }
178 }
179 Ok(())
180}
181
182fn ensure_unique_widgets(widgets: &[AdminWidgetDescriptor]) -> Result<(), AdminModelError> {
183 let mut seen = HashSet::new();
184 for widget in widgets {
185 if !seen.insert(widget.id.clone()) {
186 return Err(AdminModelError::DuplicateWidget {
187 widget_id: widget.id.to_string(),
188 });
189 }
190 }
191 Ok(())
192}
193
194fn ensure_unique_workflows(workflows: &[WorkflowAction]) -> Result<(), AdminModelError> {
195 let mut seen = HashSet::new();
196 for workflow in workflows {
197 if !seen.insert(workflow.id.clone()) {
198 return Err(AdminModelError::DuplicateWorkflow {
199 workflow_id: workflow.id.to_string(),
200 });
201 }
202 }
203 Ok(())
204}