Skip to content

Commit 1df15a3

Browse files
committed
feat: adds additional options to component list endpoint into luminork
1 parent 4c2ffa6 commit 1df15a3

File tree

4 files changed

+140
-18
lines changed

4 files changed

+140
-18
lines changed

lib/luminork-server/src/service/v1.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub use components::{
8080
list_components::{
8181
ComponentDetailsV1,
8282
ListComponentsV1Response,
83+
QualificationStatusV1 as ListComponentsQualificationStatusV1,
8384
},
8485
manage_component::{
8586
ManageComponentV1Request,
@@ -313,6 +314,7 @@ pub use crate::api_types::func_run::v1::{
313314
SourceViewV1,
314315
ComponentDetailsV1,
315316
ListComponentsV1Response,
317+
ListComponentsQualificationStatusV1,
316318
FindComponentV1Params,
317319
FuncRunV1RequestPath,
318320
FuncRunLogViewV1,

lib/luminork-server/src/service/v1/common.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,8 @@ pub struct PaginationParams {
136136
pub cursor: Option<String>,
137137
#[schema(value_type = Option<bool>)]
138138
pub include_codegen: Option<bool>,
139+
#[schema(value_type = Option<bool>, example = "true")]
140+
pub include_qualifications: Option<bool>,
141+
#[schema(value_type = Option<bool>, example = "true")]
142+
pub include_upgrade_status: Option<bool>,
139143
}

lib/luminork-server/src/service/v1/components/list_components.rs

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,27 @@ use crate::{
2727
};
2828

2929
#[derive(Serialize, Debug, ToSchema)]
30-
#[serde(rename_all = "camelCase")]
3130
pub struct ListComponentsV1Response {
3231
#[schema(
3332
value_type = Vec<ComponentDetailsV1>,
3433
example = json!([
3534
{
36-
"component_id": "01H9ZQD35JPMBGHH69BT0Q79AA",
35+
"componentId": "01H9ZQD35JPMBGHH69BT0Q79AA",
3736
"name": "my-vpc",
38-
"schema_name": "AWS::EC2::VPC"
37+
"schemaName": "AWS::EC2::VPC"
3938
},
4039
{
41-
"component_id": "01H9ZQD35JPMBGHH69BT0Q79BB",
40+
"componentId": "01H9ZQD35JPMBGHH69BT0Q79BB",
4241
"name": "Public 1",
43-
"schema_name": "AWS::EC2::Subnet"
42+
"schemaName": "AWS::EC2::Subnet",
43+
"qualificationStatus": {
44+
"total": 2,
45+
"succeeded": 1,
46+
"warned": 0,
47+
"failed": 0,
48+
"running": 1
49+
},
50+
"canBeUpgraded": true
4451
}
4552
])
4653
)]
@@ -56,8 +63,34 @@ pub struct ComponentDetailsV1 {
5663
pub name: String,
5764
pub schema_name: String,
5865
pub codegen: Option<Value>,
66+
#[serde(skip_serializing_if = "Option::is_none")]
67+
pub qualification_status: Option<QualificationStatusV1>,
68+
#[serde(skip_serializing_if = "Option::is_none")]
69+
pub can_be_upgraded: Option<bool>,
70+
}
71+
72+
#[derive(Serialize, Debug, ToSchema)]
73+
pub struct QualificationStatusV1 {
74+
pub total: u64,
75+
pub succeeded: u64,
76+
pub warned: u64,
77+
pub failed: u64,
78+
pub running: u64,
5979
}
6080

81+
impl From<si_frontend_types::ComponentQualificationStats> for QualificationStatusV1 {
82+
fn from(stats: si_frontend_types::ComponentQualificationStats) -> Self {
83+
Self {
84+
total: stats.total,
85+
succeeded: stats.succeeded,
86+
warned: stats.warned,
87+
failed: stats.failed,
88+
running: stats.running,
89+
}
90+
}
91+
}
92+
93+
6194
#[utoipa::path(
6295
get,
6396
path = "/v1/w/{workspace_id}/change-sets/{change_set_id}/components",
@@ -67,21 +100,31 @@ pub struct ComponentDetailsV1 {
67100
("limit" = Option<String>, Query, description = "Maximum number of results to return (default: 50, max: 300)"),
68101
("cursor" = Option<String>, Query, description = "Cursor for pagination (ComponentId of the last item from previous page)"),
69102
("includeCodegen" = Option<bool>, Query, description = "Allow returning the codegen for the cloudformation template for the component (if it exists)"),
103+
("includeQualifications" = Option<bool>, Query, description = "Include real-time qualification status"),
104+
("includeUpgradeStatus" = Option<bool>, Query, description = "Include upgrade-ability information"),
70105
),
71106
summary = "List all components",
72107
tag = "components",
73108
responses(
74109
(status = 200, description = "Components retrieved successfully", body = ListComponentsV1Response, example = json!({
75110
"componentDetails": [
76111
{
77-
"component_id": "01H9ZQD35JPMBGHH69BT0Q79AA",
112+
"componentId": "01H9ZQD35JPMBGHH69BT0Q79AA",
78113
"name": "my-vpc",
79-
"schema_name": "AWS::EC2::VPC"
114+
"schemaName": "AWS::EC2::VPC"
80115
},
81116
{
82-
"component_id": "01H9ZQD35JPMBGHH69BT0Q79BB",
117+
"componentId": "01H9ZQD35JPMBGHH69BT0Q79BB",
83118
"name": "Public 1",
84-
"schema_name": "AWS::EC2::Subnet"
119+
"schemaName": "AWS::EC2::Subnet",
120+
"qualificationStatus": {
121+
"total": 2,
122+
"succeeded": 1,
123+
"warned": 0,
124+
"failed": 0,
125+
"running": 1
126+
},
127+
"canBeUpgraded": true
85128
}
86129
],
87130
"nextCursor": null
@@ -143,21 +186,32 @@ pub async fn list_components(
143186
name,
144187
schema_name,
145188
codegen: None,
189+
qualification_status: None,
190+
can_be_upgraded: None,
146191
};
147192

148-
if let Some(codegen) = params.include_codegen {
149-
if codegen {
150-
let code_map_av_id =
151-
Component::find_code_map_attribute_value_id(ctx, component.id()).await?;
193+
// Handle existing includeCodegen parameter for backward compatibility
194+
if let Some(true) = params.include_codegen {
195+
let code_map_av_id =
196+
Component::find_code_map_attribute_value_id(ctx, component.id()).await?;
152197

153-
let view = AttributeValue::view(ctx, code_map_av_id).await?;
154-
if let Some(v) = view {
155-
let details = v.get("awsCloudFormationLint");
156-
comp_response.codegen = details.cloned();
157-
}
198+
let view = AttributeValue::view(ctx, code_map_av_id).await?;
199+
if let Some(v) = view {
200+
let details = v.get("awsCloudFormationLint");
201+
comp_response.codegen = details.cloned();
158202
}
159203
}
160204

205+
// Handle new inclusion parameters
206+
if let Some(true) = params.include_qualifications {
207+
let stats = super::get_qualification_stats_with_realtime_running(ctx, component.id()).await?;
208+
comp_response.qualification_status = Some(stats);
209+
}
210+
211+
if let Some(true) = params.include_upgrade_status {
212+
comp_response.can_be_upgraded = Some(component.can_be_upgraded(ctx).await?);
213+
}
214+
161215
comp_details.push(comp_response);
162216
}
163217

lib/luminork-server/src/service/v1/components/mod.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,64 @@ pub mod search_components;
7878
pub mod update_component;
7979
pub mod upgrade_component;
8080

81+
// Shared utility for real-time qualification tracking
82+
pub async fn get_qualification_stats_with_realtime_running(
83+
ctx: &dal::DalContext,
84+
component_id: ComponentId,
85+
) -> ComponentsResult<list_components::QualificationStatusV1> {
86+
use dal::qualification::QualificationSummary;
87+
use si_events::FuncRunState;
88+
89+
// Get base stats using existing logic
90+
let base_stats = QualificationSummary::individual_stats(ctx, component_id).await?;
91+
92+
// Count qualifications that are actively executing
93+
let qualification_avs = dal::Component::list_qualification_avs(ctx, component_id).await?;
94+
let mut active_running_count = 0;
95+
96+
for qualification_av in qualification_avs {
97+
if let Some(func_run) = ctx
98+
.layer_db()
99+
.func_run()
100+
.get_last_qualification_for_attribute_value_id(
101+
ctx.events_tenancy().workspace_pk,
102+
qualification_av.id(),
103+
)
104+
.await?
105+
{
106+
// Check if function is currently executing
107+
match func_run.state() {
108+
FuncRunState::Created
109+
| FuncRunState::Dispatched
110+
| FuncRunState::Running
111+
| FuncRunState::PostProcessing => {
112+
active_running_count += 1;
113+
}
114+
FuncRunState::Success | FuncRunState::Failure | FuncRunState::Killed => {
115+
// Completed - don't count as running
116+
}
117+
}
118+
}
119+
}
120+
121+
// If we have actively running qualifications, adjust the counts
122+
let mut result_stats = list_components::QualificationStatusV1::from(base_stats);
123+
124+
if active_running_count > 0 {
125+
// Override the running count with actual execution state
126+
result_stats.running = active_running_count;
127+
128+
// Optionally adjust other counts to maintain total consistency
129+
let accounted_for = result_stats.succeeded + result_stats.warned + result_stats.failed + result_stats.running;
130+
if accounted_for > result_stats.total {
131+
// Some qualifications are re-running, adjust previous counts
132+
result_stats.running = active_running_count.min(result_stats.total);
133+
}
134+
}
135+
136+
Ok(result_stats)
137+
}
138+
81139
#[remain::sorted]
82140
#[derive(Debug, Error)]
83141
pub enum ComponentsError {
@@ -125,6 +183,8 @@ pub enum ComponentsError {
125183
InputSocket(#[from] dal::socket::input::InputSocketError),
126184
#[error("invalid secret value: {0}")]
127185
InvalidSecretValue(String),
186+
#[error("layer db error: {0}")]
187+
LayerDb(#[from] si_layer_cache::LayerDbError),
128188
#[error("management func error: {0}")]
129189
ManagementFuncExecution(#[from] si_db::ManagementFuncExecutionError),
130190
#[error("management function already running for this component")]
@@ -147,6 +207,8 @@ pub enum ComponentsError {
147207
OutputSocket(#[from] dal::socket::output::OutputSocketError),
148208
#[error("prop error: {0}")]
149209
Prop(#[from] dal::prop::PropError),
210+
#[error("qualification summary error: {0}")]
211+
QualificationSummary(#[from] dal::qualification::QualificationSummaryError),
150212
#[error("schema error: {0}")]
151213
Schema(#[from] dal::SchemaError),
152214
#[error("schema not found by name error: {0}")]

0 commit comments

Comments
 (0)