-
+
();
+const inProgress = computed(() =>
+ props.dependentValues?.componentAttributes?.[props.component.id]?.includes(
+ "/qualification",
+ ),
+);
+
const qualificationStatus = computed(() => {
+ if (inProgress.value) return "running";
if (props.component.qualificationTotals.failed > 0) return "failure";
if (props.component.qualificationTotals.warned > 0) return "warning";
if (props.component.qualificationTotals.succeeded > 0) return "success";
diff --git a/app/web/src/newhotness/QualificationPanel.vue b/app/web/src/newhotness/QualificationPanel.vue
index 627e7f1403..542478072f 100644
--- a/app/web/src/newhotness/QualificationPanel.vue
+++ b/app/web/src/newhotness/QualificationPanel.vue
@@ -21,6 +21,7 @@ import { computed } from "vue";
import {
AttributeTree,
BifrostComponent,
+ DependentValues,
} from "@/workers/types/entity_kind_types";
import QualificationView from "@/newhotness/QualificationView.vue";
import { AttributeValueId } from "./types";
@@ -37,54 +38,51 @@ export interface Qualification {
// This exists so the validation qualification can pass in its output, since that comes baked in the data
// We should avoid using it for normal qualifications, for which QualificationView will lazily fetch the output
output?: null | string[];
+ /** Indicates whether the qualification is dirty (running or waiting to run) */
+ isDirty?: boolean;
}
const props = defineProps<{
component: BifrostComponent;
- attributeTree?: AttributeTree;
+ attributeTree?: AttributeTree | null;
+ dependentValues?: DependentValues | null;
}>();
-const root = computed(() => props.attributeTree);
-
const qualifications = computed(() => {
- const items: Qualification[] = [];
- const r = root.value;
- if (!r) return items;
- const data = findAvsAtPropPath(r, [
+ const root = props.attributeTree;
+ if (!root) return [];
+
+ const qualificationItems = findAvsAtPropPath(root, [
"root",
"qualification",
"qualificationItem",
]);
- if (data) {
- const { attributeValues } = data;
-
- attributeValues.forEach((av) => {
- const name = av.key;
- const children = r.treeInfo[av.id]?.children ?? [];
- let status;
- let message;
- children.forEach((avId) => {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const child = r.attributeValues[avId]!;
- if (child.path?.endsWith("result")) status = child.value;
- else if (child.path?.endsWith("message")) message = child.value;
- });
- items.push({
- avId: av.id,
- name,
- status,
- message,
- });
- });
- }
+ if (!qualificationItems) return [];
+ const items = qualificationItems.attributeValues.map((av) => {
+ const qualification = { avId: av.id, name: av.key } as Qualification;
+ // Set result and message based on the AttributeTree
+ for (const avId of root.treeInfo[av.id]?.children ?? []) {
+ const child = root.attributeValues[avId];
+ // TODO should we set both if they both exist?
+ if (child?.path?.endsWith("result"))
+ qualification.status = child.value as QualificationStatus;
+ else if (child?.path?.endsWith("message"))
+ qualification.message = child.value as string;
+ }
+ // Set isDirty based on dependentValues
+ qualification.isDirty = props.dependentValues?.componentAttributes[
+ props.component.id
+ ]?.includes(av.path);
+ return qualification;
+ });
// Since we have all the data locally, we compute the validation rollup qualification over here
// The qualification also gets computed in the backed for the old UI and luminork, so at some point we may
// revisit this, but this works well.
let hasValidations = false;
const validationOutput: string[] = [];
- Object.values(r.attributeValues).forEach((av) => {
- const prop = r.props[av.propId ?? ""];
+ Object.values(root.attributeValues).forEach((av) => {
+ const prop = root.props[av.propId ?? ""];
if (!av.validation || !prop) return;
hasValidations = true;
diff --git a/app/web/src/newhotness/QualificationView.vue b/app/web/src/newhotness/QualificationView.vue
index ffe9eb958f..cfc1d32971 100644
--- a/app/web/src/newhotness/QualificationView.vue
+++ b/app/web/src/newhotness/QualificationView.vue
@@ -18,7 +18,12 @@
:status="qualificationStatus ?? 'unknown'"
class="break-all"
>
- Passed!
+
+ The qualification has not yet run or is actively running.
+
+
+ Passed!
+
+
- The qualification has not yet ran or is actively running.
+ The qualification has not yet run or is actively running.
diff --git a/app/web/src/workers/types/entity_kind_types.ts b/app/web/src/workers/types/entity_kind_types.ts
index b8e77bf7e3..51f3fba0c7 100644
--- a/app/web/src/workers/types/entity_kind_types.ts
+++ b/app/web/src/workers/types/entity_kind_types.ts
@@ -19,6 +19,7 @@ import { ChangeSetId } from "@/api/sdf/dal/change_set";
import { DefaultMap } from "@/utils/defaultmap";
import { ComponentName } from "@/store/components.store";
import { WorkspacePk } from "@/newhotness/types";
+import { SecretId } from "@/store/realtime/realtime_events";
import { ComponentInfo } from "./dbinterface";
export enum EntityKind {
@@ -36,6 +37,7 @@ export enum EntityKind {
ComponentsInViews = "ComponentsInViews",
DefaultSubscriptions = "DefaultSubscriptions",
DependentValueComponentList = "DependentValueComponentList",
+ DependentValues = "DependentValues",
ErasedComponents = "ErasedComponents",
IncomingConnections = "IncomingConnections",
IncomingConnectionsList = "IncomingConnectionsList",
@@ -220,7 +222,7 @@ export interface SchemaMembers {
}
export interface UninstalledVariant {
- schemaId: string;
+ schemaId: SchemaId;
schemaName: string;
displayName: string | null;
category: string;
@@ -666,7 +668,7 @@ export interface SecretDefinition {
export interface Secret {
// The [`id`](SecretId) of a [`Secret`].
- id: string;
+ id: SecretId;
// The name of a [`Secret`] as provided by the user.
name: string;
// The definition of a [`Secret`].
@@ -678,10 +680,15 @@ export interface Secret {
}
export interface DependentValueComponentList {
- id: string;
+ id: WorkspacePk;
componentIds: ComponentId[];
}
+export interface DependentValues {
+ id: WorkspacePk;
+ componentAttributes: Record
;
+}
+
export interface DefaultSubscription {
componentId: ComponentId;
path: string;
diff --git a/lib/dal-materialized-views/src/dependent_values.rs b/lib/dal-materialized-views/src/dependent_values.rs
new file mode 100644
index 0000000000..cf93cc159a
--- /dev/null
+++ b/lib/dal-materialized-views/src/dependent_values.rs
@@ -0,0 +1,39 @@
+use dal::{
+ AttributeValue,
+ DalContext,
+ attribute::value::DependentValueGraph,
+ workspace_snapshot::DependentValueRoot,
+};
+use si_frontend_mv_types::dependent_values::DependentValues as DependentValuesMv;
+use telemetry::prelude::*;
+
+#[instrument(
+ name = "dal_materialized_views.dependent_values",
+ level = "debug",
+ skip_all
+)]
+pub async fn assemble(ctx: DalContext) -> super::Result {
+ let ctx = &ctx;
+ let dependent_value_graph = DependentValueGraph::new(
+ ctx,
+ DependentValueRoot::get_dependent_value_roots(ctx).await?,
+ )
+ .await?;
+
+ let mut result = DependentValuesMv {
+ id: ctx.workspace_pk()?,
+ component_attributes: Default::default(),
+ };
+
+ for dependent_value in dependent_value_graph.all_value_ids() {
+ let (component_id, path) =
+ AttributeValue::path_from_component(ctx, dependent_value.attribute_value_id()).await?;
+ result
+ .component_attributes
+ .entry(component_id)
+ .or_default()
+ .push(path);
+ }
+
+ Ok(result)
+}
diff --git a/lib/dal-materialized-views/src/lib.rs b/lib/dal-materialized-views/src/lib.rs
index 433abfdbe6..df100a728b 100644
--- a/lib/dal-materialized-views/src/lib.rs
+++ b/lib/dal-materialized-views/src/lib.rs
@@ -57,6 +57,7 @@ pub mod cached;
pub mod component;
pub mod component_list;
pub mod dependent_value_component_list;
+pub mod dependent_values;
pub mod incoming_connections;
pub mod incoming_connections_list;
pub mod luminork;
diff --git a/lib/dal/src/attribute/value.rs b/lib/dal/src/attribute/value.rs
index 42beaff61e..ad474c56b1 100644
--- a/lib/dal/src/attribute/value.rs
+++ b/lib/dal/src/attribute/value.rs
@@ -2477,6 +2477,15 @@ impl AttributeValue {
Ok((child_id, pointer.to_string()))
}
+ pub async fn path_from_component(
+ ctx: &DalContext,
+ child_av_id: AttributeValueId,
+ ) -> AttributeValueResult<(ComponentId, String)> {
+ let (root_av_id, path) = Self::path_from_root(ctx, child_av_id).await?;
+ let component_id = Self::component_id(ctx, root_av_id).await?;
+ Ok((component_id, path))
+ }
+
/// Get a subscription to this AV, from its root AV.
pub async fn as_subscription(
ctx: &DalContext,
diff --git a/lib/edda-server/src/materialized_view.rs b/lib/edda-server/src/materialized_view.rs
index dcd6c7cbce..34d5e0829e 100644
--- a/lib/edda-server/src/materialized_view.rs
+++ b/lib/edda-server/src/materialized_view.rs
@@ -52,7 +52,10 @@ use si_frontend_mv_types::{
component_diff::ComponentDiff as ComponentDiffMv,
erased_components::ErasedComponents as ErasedComponentsMv,
},
- dependent_values::DependentValueComponentList as DependentValueComponentListMv,
+ dependent_values::{
+ DependentValueComponentList as DependentValueComponentListMv,
+ DependentValues as DependentValuesMv,
+ },
incoming_connections::{
IncomingConnections as IncomingConnectionsMv,
IncomingConnectionsList as IncomingConnectionsListMv,
@@ -650,6 +653,23 @@ pub async fn spawn_build_mv_task_for_change_and_mv_kind(
(*maybe_mv_index).clone(),
);
}
+ ReferenceKind::DependentValues => {
+ let workspace_mv_id = workspace_pk.to_string();
+ metric!(
+ counter.edda.mv_build = 1,
+ label = format!("{workspace_pk}:{change_set_id_for_metrics_only}:{mv_kind}")
+ );
+ spawn_build_mv_task!(
+ build_tasks,
+ ctx,
+ frigg,
+ change,
+ workspace_mv_id,
+ DependentValuesMv,
+ dal_materialized_views::dependent_values::assemble(ctx.clone()),
+ (*maybe_mv_index).clone(),
+ );
+ }
ReferenceKind::IncomingConnections => {
let entity_mv_id = change.entity_id.to_string();
metric!(
diff --git a/lib/si-events-rs/src/materialized_view.rs b/lib/si-events-rs/src/materialized_view.rs
index 254f68ea05..e19a449fc0 100644
--- a/lib/si-events-rs/src/materialized_view.rs
+++ b/lib/si-events-rs/src/materialized_view.rs
@@ -6,7 +6,7 @@
/// * List
/// * Detail
///
-/// The order of the priorities is determined by the descriminant, largest to smallest.
+/// The order of the priorities is determined by the discriminant, largest to smallest.
pub enum BuildPriority {
#[default]
Detail = 5,
diff --git a/lib/si-frontend-mv-types-rs/src/dependent_values.rs b/lib/si-frontend-mv-types-rs/src/dependent_values.rs
index a6db803a45..04f8a4dd71 100644
--- a/lib/si-frontend-mv-types-rs/src/dependent_values.rs
+++ b/lib/si-frontend-mv-types-rs/src/dependent_values.rs
@@ -1,4 +1,9 @@
-use serde::Serialize;
+use std::collections::HashMap;
+
+use serde::{
+ Deserialize,
+ Serialize,
+};
use si_events::workspace_snapshot::EntityKind;
use si_id::{
ComponentId,
@@ -10,6 +15,7 @@ use crate::reference::ReferenceKind;
#[derive(
Debug,
Serialize,
+ Deserialize,
PartialEq,
Eq,
Clone,
@@ -28,3 +34,27 @@ pub struct DependentValueComponentList {
pub id: WorkspacePk,
pub component_ids: Vec,
}
+
+#[derive(
+ Debug,
+ Serialize,
+ Deserialize,
+ PartialEq,
+ Eq,
+ Clone,
+ si_frontend_mv_types_macros::MV,
+ si_frontend_mv_types_macros::FrontendChecksum,
+ si_frontend_mv_types_macros::FrontendObject,
+ si_frontend_mv_types_macros::Refer,
+)]
+#[serde(rename_all = "camelCase")]
+#[mv(
+ trigger_entity = EntityKind::CategoryDependentValueRoots,
+ reference_kind = ReferenceKind::DependentValues,
+ build_priority = "List",
+)]
+pub struct DependentValues {
+ pub id: WorkspacePk,
+ /// Mapping from component ID to the list of "dirty" attribute paths in that component.
+ pub component_attributes: HashMap>,
+}
diff --git a/lib/si-frontend-mv-types-rs/src/reference.rs b/lib/si-frontend-mv-types-rs/src/reference.rs
index 628049f0dd..d3a220e288 100644
--- a/lib/si-frontend-mv-types-rs/src/reference.rs
+++ b/lib/si-frontend-mv-types-rs/src/reference.rs
@@ -52,6 +52,7 @@ pub enum ReferenceKind {
ComponentInList,
ComponentList,
DependentValueComponentList,
+ DependentValues,
DeploymentMvIndex,
ErasedComponents,
IncomingConnections,