mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
fix: eval templates removal (#12595)
1. This PR removes Evaluation templates from pipeline module 2. removes LLM evaluation node from pipeline editor 3. remove LLM evaluation creation tab from schema details --------- Co-authored-by: Ashish Kolhe <35160958+oasisk@users.noreply.github.com> Co-authored-by: udev <uddhav@openobserve.ai>
This commit is contained in:
co-authored by
Ashish Kolhe
udev
parent
6ea4b39141
commit
ff8a579bdc
@@ -137,7 +137,7 @@ pub async fn save_pipeline(
|
||||
}
|
||||
let pipeline_id = pipeline.id.to_string();
|
||||
let pipeline_name = pipeline.name.clone();
|
||||
match pipeline::save_pipeline(pipeline).await {
|
||||
match pipeline::save_user_pipeline(pipeline).await {
|
||||
Ok(()) => MetaHttpResponse::json(
|
||||
MetaHttpResponse::message(StatusCode::OK, "Pipeline created successfully")
|
||||
.with_id(pipeline_id)
|
||||
@@ -206,7 +206,7 @@ pub async fn list_pipelines(
|
||||
// Get List of allowed objects ends
|
||||
}
|
||||
|
||||
let pipelines = match pipeline::list_pipelines(&org_id, _permitted).await {
|
||||
let pipelines = match pipeline::list_user_pipelines(&org_id, _permitted).await {
|
||||
Ok(pipelines) => pipelines,
|
||||
Err(e) => return e.into(),
|
||||
};
|
||||
@@ -271,15 +271,7 @@ pub async fn list_pipelines(
|
||||
)
|
||||
)]
|
||||
pub async fn get_pipeline(Path((org_id, pipeline_id)): Path<(String, String)>) -> Response {
|
||||
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
|
||||
.await
|
||||
.as_deref()
|
||||
!= Some(org_id.as_str())
|
||||
{
|
||||
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
|
||||
}
|
||||
|
||||
let meta_pipeline = match crate::service::db::pipeline::get_by_id(&pipeline_id).await {
|
||||
let meta_pipeline = match pipeline::get_user_pipeline(&org_id, &pipeline_id).await {
|
||||
Ok(pipeline) => pipeline,
|
||||
Err(e) => return e.into(),
|
||||
};
|
||||
@@ -378,15 +370,7 @@ pub async fn list_streams_with_pipeline(Path(org_id): Path<String>) -> Response
|
||||
)
|
||||
)]
|
||||
pub async fn delete_pipeline(Path((org_id, pipeline_id)): Path<(String, String)>) -> Response {
|
||||
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
|
||||
.await
|
||||
.as_deref()
|
||||
!= Some(org_id.as_str())
|
||||
{
|
||||
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
|
||||
}
|
||||
|
||||
match pipeline::delete_pipeline(&pipeline_id).await {
|
||||
match pipeline::delete_user_pipeline(&org_id, &pipeline_id).await {
|
||||
Ok(()) => MetaHttpResponse::json(MetaHttpResponse::message(
|
||||
StatusCode::OK,
|
||||
"Pipeline deleted successfully",
|
||||
@@ -451,7 +435,7 @@ pub async fn delete_pipeline_bulk(
|
||||
let mut err = None;
|
||||
|
||||
for id in req.ids {
|
||||
match pipeline::delete_pipeline(&id).await {
|
||||
match pipeline::delete_user_pipeline(&org_id, &id).await {
|
||||
Ok(()) => {
|
||||
successful.push(id);
|
||||
}
|
||||
@@ -504,7 +488,8 @@ pub async fn update_pipeline(
|
||||
Json(mut pipeline): Json<Pipeline>,
|
||||
) -> Response {
|
||||
pipeline.org = org_id;
|
||||
match pipeline::update_pipeline(pipeline).await {
|
||||
let org_id = pipeline.org.clone();
|
||||
match pipeline::update_user_pipeline(&org_id, pipeline).await {
|
||||
Ok(()) => MetaHttpResponse::json(MetaHttpResponse::message(
|
||||
StatusCode::OK,
|
||||
"Pipeline updated successfully",
|
||||
@@ -545,13 +530,6 @@ pub async fn enable_pipeline(
|
||||
Path((org_id, pipeline_id)): Path<(String, String)>,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
) -> Response {
|
||||
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
|
||||
.await
|
||||
.as_deref()
|
||||
!= Some(org_id.as_str())
|
||||
{
|
||||
return MetaHttpResponse::not_found(format!("Pipeline not found: {pipeline_id}"));
|
||||
}
|
||||
let enable = query
|
||||
.get("value")
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
@@ -562,7 +540,7 @@ pub async fn enable_pipeline(
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
match pipeline::enable_pipeline(&org_id, &pipeline_id, enable, starts_from_now).await {
|
||||
match pipeline::enable_user_pipeline(&org_id, &pipeline_id, enable, starts_from_now).await {
|
||||
Ok(()) => {
|
||||
let resp_msg = format!(
|
||||
"Pipeline successfully {}",
|
||||
@@ -644,7 +622,7 @@ pub async fn enable_pipeline_bulk(
|
||||
let mut err = None;
|
||||
|
||||
for id in req.ids {
|
||||
match pipeline::enable_pipeline(&org_id, &id, enable, starts_from_now).await {
|
||||
match pipeline::enable_user_pipeline(&org_id, &id, enable, starts_from_now).await {
|
||||
Ok(()) => {
|
||||
successful.push(id);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ use crate::common::meta::http::HttpResponse as MetaHttpResponse;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::service::alerts::backfill;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn ensure_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<(), Response> {
|
||||
crate::service::pipeline::get_user_pipeline(org_id, pipeline_id)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct BackfillRequest {
|
||||
/// Start time in microseconds
|
||||
@@ -83,12 +91,8 @@ pub async fn create_backfill(
|
||||
Path((org_id, pipeline_id)): Path<(String, String)>,
|
||||
Json(req): Json<BackfillRequest>,
|
||||
) -> Response {
|
||||
if crate::service::db::pipeline::get_org_by_id(&pipeline_id)
|
||||
.await
|
||||
.as_deref()
|
||||
!= Some(org_id.as_str())
|
||||
{
|
||||
return MetaHttpResponse::not_found(format!("Pipeline with id {pipeline_id} not found"));
|
||||
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
@@ -244,6 +248,10 @@ pub async fn list_backfills(Path(_org_id): Path<String>) -> Response {
|
||||
pub async fn get_backfill(
|
||||
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
|
||||
) -> Response {
|
||||
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
match backfill::get_backfill_job(&org_id, &job_id).await {
|
||||
Ok(job) => {
|
||||
// Verify the job belongs to the specified pipeline
|
||||
@@ -330,6 +338,10 @@ pub async fn enable_backfill(
|
||||
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
|
||||
Query(query): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Response {
|
||||
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
let enable = query
|
||||
.get("value")
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
@@ -441,6 +453,10 @@ pub async fn enable_backfill(
|
||||
pub async fn delete_backfill(
|
||||
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
|
||||
) -> Response {
|
||||
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Verify the job belongs to the specified pipeline
|
||||
match backfill::get_backfill_job(&org_id, &job_id).await {
|
||||
Ok(job) => {
|
||||
@@ -554,6 +570,10 @@ pub async fn update_backfill(
|
||||
Path((org_id, pipeline_id, job_id)): Path<(String, String, String)>,
|
||||
Json(req): Json<BackfillRequest>,
|
||||
) -> Response {
|
||||
if let Err(response) = ensure_user_pipeline(&org_id, &pipeline_id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Verify the job belongs to the specified pipeline
|
||||
match backfill::get_backfill_job(&org_id, &job_id).await {
|
||||
Ok(job) => {
|
||||
|
||||
@@ -44,7 +44,7 @@ use crate::{
|
||||
},
|
||||
handler::http::{extractors::Headers, request::alerts::history::escape_like},
|
||||
service::{
|
||||
db::pipeline::list_by_org as list_pipelines,
|
||||
pipeline::list_user_pipelines,
|
||||
search::{self as SearchService},
|
||||
},
|
||||
};
|
||||
@@ -562,7 +562,7 @@ pub async fn get_pipeline_history(
|
||||
/// Helper function to get all pipeline IDs for an organization
|
||||
async fn get_organization_pipeline_ids(org_id: &str) -> Result<Vec<String>, anyhow::Error> {
|
||||
// Get all pipelines for the organization
|
||||
let pipelines = list_pipelines(org_id).await?;
|
||||
let pipelines = list_user_pipelines(org_id, None).await?;
|
||||
|
||||
// Extract pipeline IDs
|
||||
let ids: Vec<String> = pipelines.into_iter().map(|pipeline| pipeline.id).collect();
|
||||
|
||||
@@ -152,7 +152,9 @@ pub async fn get_summary(org_id: &str) -> OrgSummary {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let pipelines = db::pipeline::list_by_org(org_id).await.unwrap_or_default();
|
||||
let pipelines = super::pipeline::list_user_pipelines(org_id, None)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let pipeline_summary = PipelineSummary {
|
||||
num_realtime: pipelines
|
||||
.iter()
|
||||
|
||||
+149
-1
@@ -15,7 +15,7 @@
|
||||
|
||||
use config::meta::{
|
||||
pipeline::{
|
||||
Pipeline,
|
||||
Pipeline, PipelineKind,
|
||||
components::{NodeData, PipelineSource},
|
||||
},
|
||||
search::SearchEventType,
|
||||
@@ -115,6 +115,18 @@ pub async fn save_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(pipeline))]
|
||||
pub async fn save_user_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError> {
|
||||
if let Ok(existing_pipeline) = pipeline::get_by_id(&pipeline.id).await
|
||||
&& (existing_pipeline.org != pipeline.org || !existing_pipeline.is_user())
|
||||
{
|
||||
return Err(PipelineError::NotFound(pipeline.id));
|
||||
}
|
||||
|
||||
pipeline.kind = PipelineKind::User;
|
||||
save_pipeline(pipeline).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(pipeline))]
|
||||
pub async fn update_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError> {
|
||||
let Ok(existing_pipeline) = pipeline::get_by_id(&pipeline.id).await else {
|
||||
@@ -212,6 +224,16 @@ pub async fn update_pipeline(mut pipeline: Pipeline) -> Result<(), PipelineError
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(pipeline))]
|
||||
pub async fn update_user_pipeline(
|
||||
org_id: &str,
|
||||
mut pipeline: Pipeline,
|
||||
) -> Result<(), PipelineError> {
|
||||
get_user_pipeline(org_id, &pipeline.id).await?;
|
||||
pipeline.kind = PipelineKind::User;
|
||||
update_pipeline(pipeline).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list_pipelines(
|
||||
org_id: &str,
|
||||
@@ -234,6 +256,45 @@ pub async fn list_pipelines(
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list_user_pipelines(
|
||||
org_id: &str,
|
||||
permitted: Option<Vec<String>>,
|
||||
) -> Result<Vec<Pipeline>, PipelineError> {
|
||||
Ok(pipeline::list_by_org(org_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|pipeline| is_user_pipeline_visible(pipeline, org_id, permitted.as_ref()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn is_user_pipeline_visible(
|
||||
pipeline: &Pipeline,
|
||||
org_id: &str,
|
||||
permitted: Option<&Vec<String>>,
|
||||
) -> bool {
|
||||
if !pipeline.is_user() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match permitted {
|
||||
Some(permitted) => {
|
||||
permitted.contains(&format!("pipeline:{}", pipeline.id))
|
||||
|| permitted.contains(&format!("pipeline:_all_{org_id}"))
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<Pipeline, PipelineError> {
|
||||
let pipeline = pipeline::get_by_id(pipeline_id).await?;
|
||||
if pipeline.org != org_id || !pipeline.is_user() {
|
||||
return Err(PipelineError::NotFound(pipeline_id.to_string()));
|
||||
}
|
||||
Ok(pipeline)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list_pipeline_triggers(org_id: &str) -> Result<Vec<Trigger>, PipelineError> {
|
||||
Ok(
|
||||
@@ -294,6 +355,17 @@ pub async fn enable_pipeline(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn enable_user_pipeline(
|
||||
org_id: &str,
|
||||
pipeline_id: &str,
|
||||
enable: bool,
|
||||
starts_from_now: bool,
|
||||
) -> Result<(), PipelineError> {
|
||||
get_user_pipeline(org_id, pipeline_id).await?;
|
||||
enable_pipeline(org_id, pipeline_id, enable, starts_from_now).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_pipeline(pipeline_id: &str) -> Result<(), PipelineError> {
|
||||
let Ok(existing_pipeline) = pipeline::get_by_id(pipeline_id).await else {
|
||||
@@ -337,3 +409,79 @@ pub async fn delete_pipeline(pipeline_id: &str) -> Result<(), PipelineError> {
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_user_pipeline(org_id: &str, pipeline_id: &str) -> Result<(), PipelineError> {
|
||||
get_user_pipeline(org_id, pipeline_id).await?;
|
||||
delete_pipeline(pipeline_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::meta::{
|
||||
pipeline::{
|
||||
Pipeline, PipelineKind,
|
||||
components::{DerivedStream, PipelineSource},
|
||||
},
|
||||
stream::StreamParams,
|
||||
};
|
||||
|
||||
use super::is_user_pipeline_visible;
|
||||
|
||||
fn test_pipeline(id: &str, kind: PipelineKind) -> Pipeline {
|
||||
Pipeline {
|
||||
id: id.to_string(),
|
||||
version: 1,
|
||||
enabled: true,
|
||||
org: "test_org".to_string(),
|
||||
name: id.to_string(),
|
||||
description: String::new(),
|
||||
source: PipelineSource::Realtime(StreamParams::default()),
|
||||
nodes: vec![],
|
||||
edges: vec![],
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_user_pipeline_visible_excludes_evaluation_pipelines() {
|
||||
let user_pipeline = test_pipeline("user_pipeline", PipelineKind::User);
|
||||
let evaluation_pipeline = test_pipeline("evaluation_pipeline", PipelineKind::Evaluation);
|
||||
|
||||
assert!(is_user_pipeline_visible(&user_pipeline, "test_org", None));
|
||||
assert!(!is_user_pipeline_visible(
|
||||
&evaluation_pipeline,
|
||||
"test_org",
|
||||
None
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_user_pipeline_visible_applies_permissions() {
|
||||
let pipeline = test_pipeline("user_pipeline", PipelineKind::User);
|
||||
|
||||
assert!(is_user_pipeline_visible(
|
||||
&pipeline,
|
||||
"test_org",
|
||||
Some(&vec!["pipeline:user_pipeline".to_string()])
|
||||
));
|
||||
assert!(is_user_pipeline_visible(
|
||||
&pipeline,
|
||||
"test_org",
|
||||
Some(&vec!["pipeline:_all_test_org".to_string()])
|
||||
));
|
||||
assert!(!is_user_pipeline_visible(
|
||||
&pipeline,
|
||||
"test_org",
|
||||
Some(&vec!["pipeline:other_pipeline".to_string()])
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_user_pipeline_visible_keeps_scheduled_user_pipelines() {
|
||||
let mut pipeline = test_pipeline("scheduled_pipeline", PipelineKind::User);
|
||||
pipeline.source = PipelineSource::Scheduled(DerivedStream::default());
|
||||
|
||||
assert!(is_user_pipeline_visible(&pipeline, "test_org", None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2098,22 +2098,6 @@ describe("Schema Component Tests", () => {
|
||||
expect(names.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should compute llmEvalStreamFields", () => {
|
||||
const fields = wrapper.vm.llmEvalStreamFields;
|
||||
expect(Array.isArray(fields)).toBe(true);
|
||||
expect(fields.length).toBe(3);
|
||||
expect(fields[0]).toHaveProperty("label");
|
||||
expect(fields[0]).toHaveProperty("value");
|
||||
});
|
||||
|
||||
it("should mark llmEvalFormDirty to false when tab changes from llmEvaluation", () => {
|
||||
wrapper.vm.llmEvalFormDirty = true;
|
||||
wrapper.vm.updateActiveMainTab("llmEvaluation");
|
||||
// llmEvalFormDirty stays true for llm tab
|
||||
wrapper.vm.updateActiveMainTab("schemaSettings");
|
||||
expect(wrapper.vm.llmEvalFormDirty).toBe(false);
|
||||
});
|
||||
|
||||
it("should compute hasUDSFieldInSelection", async () => {
|
||||
wrapper.vm.activeTab = "allFields";
|
||||
wrapper.vm.selectedFields = [{ name: "message", isUserDefined: true }];
|
||||
@@ -2408,12 +2392,6 @@ describe("Schema Component Tests", () => {
|
||||
|
||||
afterEach(() => { w.unmount(); });
|
||||
|
||||
it("should expose llmEvalStreamFields for traces streams", () => {
|
||||
const fields = w.vm.llmEvalStreamFields;
|
||||
expect(Array.isArray(fields)).toBe(true);
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should show cross-linking tab when enable_cross_linking is true", async () => {
|
||||
store.state.zoConfig.enable_cross_linking = true;
|
||||
w.vm.updateActiveMainTab("crossLinking");
|
||||
|
||||
@@ -333,19 +333,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
label="Configuration"
|
||||
data-test="schema-configuration-tab"
|
||||
/>
|
||||
<!-- LLM Evaluation Tab (enterprise + ai_enabled + traces only) -->
|
||||
<OTab
|
||||
v-if="
|
||||
config.isEnterprise == 'true' &&
|
||||
store.state.zoConfig.ai_enabled &&
|
||||
indexData.stream_type === 'traces'
|
||||
"
|
||||
name="llmEvaluation"
|
||||
icon="psychology"
|
||||
:label="t('pipeline.llmEvaluation')"
|
||||
data-test="stream-llm-evaluation-tab"
|
||||
/>
|
||||
|
||||
<!-- Cross-Linking Tab -->
|
||||
<OTab
|
||||
v-if="
|
||||
@@ -773,15 +760,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LLM Evaluation tab -->
|
||||
<LlmEvaluationSettings
|
||||
v-if="activeMainTab == 'llmEvaluation'"
|
||||
ref="llmEvalSettingsRef"
|
||||
:stream-name="indexData.name"
|
||||
:stream-fields="llmEvalStreamFields"
|
||||
@dirty="llmEvalFormDirty = true"
|
||||
/>
|
||||
|
||||
<!-- cross-linking tab -->
|
||||
<div v-if="activeMainTab == 'crossLinking'">
|
||||
<div class="tw:p-4">
|
||||
@@ -810,32 +788,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
<!-- floating footer for the table -->
|
||||
<div class="floating-buttons tw:flex-shrink-0 tw:px-2 tw:py-1">
|
||||
<!-- LLM Evaluation tab footer -->
|
||||
<div
|
||||
v-if="activeMainTab === 'llmEvaluation'"
|
||||
class="tw:flex tw:items-center tw:justify-end tw:gap-2"
|
||||
>
|
||||
<OButton
|
||||
data-test="schema-cancel-button"
|
||||
variant="outline"
|
||||
size="sm-action"
|
||||
@click="llmEvalFormDirty = false; $emit('close')"
|
||||
>
|
||||
{{ t("logStream.cancel") }}
|
||||
</OButton>
|
||||
<OButton
|
||||
:disabled="!llmEvalFormDirty"
|
||||
data-test="schema-update-settings-button"
|
||||
variant="primary"
|
||||
size="sm-action"
|
||||
@click="llmEvalSettingsRef?.save()"
|
||||
>
|
||||
{{ t("logStream.updateSettings") }}
|
||||
</OButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="indexData.schema.length > 0"
|
||||
v-if="indexData.schema.length > 0"
|
||||
class="tw:flex tw:items-center tw:justify-between"
|
||||
>
|
||||
<div class="tw:flex tw:items-center tw:gap-2">
|
||||
@@ -1032,7 +986,6 @@ import DateTime from "@/components/DateTime.vue";
|
||||
import AssociatedRegexPatterns from "./AssociatedRegexPatterns.vue";
|
||||
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
|
||||
import PerformanceFieldsDialog from "./PerformanceFieldsDialog.vue";
|
||||
import LlmEvaluationSettings from "./LlmEvaluationSettings.vue";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OInput from "@/lib/forms/Input/OInput.vue";
|
||||
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
||||
@@ -1082,7 +1035,6 @@ export default defineComponent({
|
||||
AssociatedRegexPatterns,
|
||||
ODrawer,
|
||||
PerformanceFieldsDialog,
|
||||
LlmEvaluationSettings,
|
||||
CrossLinkManager,
|
||||
OButton,
|
||||
OIcon,
|
||||
@@ -1137,14 +1089,6 @@ export default defineComponent({
|
||||
let previousSchemaVersion: any = null;
|
||||
const approxPartition = ref(false);
|
||||
|
||||
const llmEvalStreamFields = computed(() =>
|
||||
(indexData.value.schema || []).map((f: any) => ({
|
||||
label: f.name,
|
||||
value: f.name,
|
||||
})),
|
||||
);
|
||||
const llmEvalSettingsRef = ref<any>(null);
|
||||
const llmEvalFormDirty = ref(false);
|
||||
const streamCrossLinks = ref<any[]>([]);
|
||||
const orgCrossLinks = computed(
|
||||
() =>
|
||||
@@ -2112,7 +2056,6 @@ export default defineComponent({
|
||||
|
||||
const updateActiveMainTab = (tab) => {
|
||||
activeMainTab.value = tab;
|
||||
if (tab !== "llmEvaluation") llmEvalFormDirty.value = false;
|
||||
};
|
||||
|
||||
// Function to get missing FTS and Secondary Index fields
|
||||
@@ -2770,9 +2713,6 @@ export default defineComponent({
|
||||
quickModeIcon,
|
||||
getConfigIcon,
|
||||
getTimelineIcon,
|
||||
llmEvalStreamFields,
|
||||
llmEvalSettingsRef,
|
||||
llmEvalFormDirty,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -112,11 +112,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:open="true"
|
||||
@cancel:hideform="resetDialog"
|
||||
/>
|
||||
<LlmEvaluation
|
||||
v-if="pipelineObj.dialog.name === 'llm_evaluation'"
|
||||
:open="true"
|
||||
@cancel:hideform="resetDialog"
|
||||
/>
|
||||
<ODrawer data-test="pipeline-editor-json-editor-drawer"
|
||||
v-model:open="showJsonEditorDialog"
|
||||
:width="70"
|
||||
@@ -191,7 +186,6 @@ import useDragAndDrop from "@/plugins/pipelines/useDnD";
|
||||
import StreamNode from "@/components/pipeline/NodeForm/Stream.vue";
|
||||
import QueryForm from "@/components/pipeline/NodeForm/Query.vue";
|
||||
import ConditionForm from "@/components/pipeline/NodeForm/Condition.vue";
|
||||
import LlmEvaluation from "@/components/pipeline/NodeForm/LlmEvaluation.vue";
|
||||
import { MarkerType, useVueFlow } from "@vue-flow/core";
|
||||
import ExternalDestination from "./NodeForm/ExternalDestination.vue";
|
||||
import { contextRegistry, createPipelinesContextProvider } from "@/composables/contextProviders";
|
||||
@@ -206,7 +200,6 @@ const externalOutputImage = getImageURL("images/pipeline/output_remote.png");
|
||||
const streamRouteImage = getImageURL("images/pipeline/route.svg");
|
||||
const conditionImage = getImageURL("images/pipeline/transform_condition.png");
|
||||
const queryImage = getImageURL("images/pipeline/input_query.png");
|
||||
const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
|
||||
import useStreams from "@/composables/useStreams";
|
||||
import usePipelines from "@/composables/usePipelines";
|
||||
|
||||
@@ -494,22 +487,6 @@ watch(
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (config.isEnterprise == "true" && store.state.zoConfig.ai_enabled) {
|
||||
// Insert LLM Evaluation node before the "Destination" section header
|
||||
const destIdx = nodeTypes.findIndex(
|
||||
(n) => n.isSectionHeader && n.label === "Destination",
|
||||
);
|
||||
if (destIdx !== -1) {
|
||||
nodeTypes.splice(destIdx, 0, {
|
||||
label: "LLM Evaluation",
|
||||
subtype: "llm_evaluation",
|
||||
io_type: "default",
|
||||
icon: "img:" + llmEvaluationImage,
|
||||
tooltip: "LLM Evaluation Node",
|
||||
isSectionHeader: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (config.isEnterprise == "true") {
|
||||
nodeTypes.push({
|
||||
label: "Remote",
|
||||
|
||||
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<!--
|
||||
PipelineSectionTabs — the L2 section switcher (Stream Pipelines / Functions /
|
||||
Enrichment Tables / Eval Templates) rendered as tabs next to a page title.
|
||||
Enrichment Tables) rendered as tabs next to a page title.
|
||||
Drop it into an AppPageHeader's #tabs slot on any pipeline section page; the
|
||||
active tab is derived from the current route and clicking a tab navigates.
|
||||
This mirrors the breadcrumb section dropdown as faster inline navigation.
|
||||
@@ -57,7 +57,6 @@ import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter, type RouteLocationRaw } from "vue-router";
|
||||
import config from "@/aws-exports";
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
|
||||
import OTab from "@/lib/navigation/Tabs/OTab.vue";
|
||||
@@ -81,8 +80,6 @@ const routeToSection: Record<string, string> = {
|
||||
pipelineBackfill: "streamPipelines",
|
||||
functionList: "functions",
|
||||
enrichmentTables: "enrichmentTables",
|
||||
evalTemplates: "evalTemplates",
|
||||
evalTemplatesAdd: "evalTemplates",
|
||||
};
|
||||
|
||||
const activeSectionKey = computed(() => {
|
||||
@@ -127,13 +124,6 @@ const sections = computed<Section[]>(() => {
|
||||
to: { name: "enrichmentTables", query: q },
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: "evalTemplates",
|
||||
label: t("pipeline.evalTemplates"),
|
||||
icon: "fact-check",
|
||||
to: { name: "evalTemplates", query: q },
|
||||
visible: config.isEnterprise == "true",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
@@ -51,8 +51,6 @@ import DropzoneBackground from "@/plugins/pipelines/DropzoneBackground.vue";
|
||||
import { ControlButton, Controls } from '@vue-flow/controls'
|
||||
import { VueFlow } from "@vue-flow/core";
|
||||
import { ref, onMounted, nextTick } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import config from "@/aws-exports";
|
||||
import CustomNode from '@/plugins/pipelines/CustomNode.vue';
|
||||
import CustomEdge from "@/plugins/pipelines/CustomEdge.vue";
|
||||
/* import the required styles */
|
||||
@@ -65,7 +63,6 @@ const streamOutputImage = getImageURL("images/pipeline/output_stream.png");
|
||||
const externalOutputImage = getImageURL("images/pipeline/output_remote.png");
|
||||
const conditionImage = getImageURL("images/pipeline/transform_condition.png");
|
||||
const queryImage = getImageURL("images/pipeline/input_query.png");
|
||||
const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
@@ -74,7 +71,6 @@ const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
|
||||
},
|
||||
components: { VueFlow, CustomNode, DropzoneBackground, CustomEdge, ControlButton, Controls },
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const {
|
||||
pipelineObj,
|
||||
} = useDragAndDrop();
|
||||
@@ -167,22 +163,6 @@ const llmEvaluationImage = getImageURL("images/common/ai_icon_primary.svg");
|
||||
},
|
||||
];
|
||||
|
||||
if (config.isEnterprise == "true" && store.state.zoConfig.ai_enabled) {
|
||||
const destIdx = pipelineObj.nodeTypes.findIndex(
|
||||
(n) => n.isSectionHeader && n.label === "Destination",
|
||||
);
|
||||
if (destIdx !== -1) {
|
||||
pipelineObj.nodeTypes.splice(destIdx, 0, {
|
||||
label: "LLM Evaluation",
|
||||
subtype: "llm_evaluation",
|
||||
io_type: "default",
|
||||
icon: "img:" + llmEvaluationImage,
|
||||
tooltip: "LLM Evaluation Node",
|
||||
isSectionHeader: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Watch for pipeline prop changes to update error information
|
||||
|
||||
@@ -286,16 +286,6 @@ size="sm">
|
||||
/></template>
|
||||
Thread
|
||||
</OToggleGroupItem>
|
||||
<OToggleGroupItem
|
||||
v-if="hasLLMSpans && evalPipelineExists && evalData.length > 0"
|
||||
value="evaluations"
|
||||
size="sm"
|
||||
>
|
||||
<template #icon-left
|
||||
><OIcon name="assignment-turned-in" size="xs" class="tw:shrink-0"
|
||||
/></template>
|
||||
Evaluations
|
||||
</OToggleGroupItem>
|
||||
</OToggleGroup>
|
||||
</div>
|
||||
|
||||
@@ -737,21 +727,6 @@ size="xs"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Evaluations View - only for LLM traces with evaluation data -->
|
||||
<div
|
||||
v-if="
|
||||
hasLLMSpans && evalPipelineExists && activeTab === 'evaluations'
|
||||
"
|
||||
style="display: flex; flex: 1; min-height: 0; overflow-y: auto"
|
||||
class="tw:w-full tw:bg-[var(--o2-card-bg)]!"
|
||||
>
|
||||
<TraceEvaluationsView
|
||||
ref="traceEvaluationsViewRef"
|
||||
data-test="trace-details-evaluations"
|
||||
:eval-data="evalData"
|
||||
:is-loading="isLoadingEvalData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -868,7 +843,6 @@ import OToggleGroupItem from "@/lib/core/ToggleGroup/OToggleGroupItem.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import ODrawer from '@/lib/overlay/Drawer/ODrawer.vue';
|
||||
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
||||
import pipelineService from "@/services/pipelines";
|
||||
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
||||
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
||||
import OSearchInput from "@/lib/forms/SearchInput/OSearchInput.vue";
|
||||
@@ -891,11 +865,6 @@ const FlameGraphView = defineAsyncComponent(
|
||||
() => import("@/components/traces/FlameGraphView.vue"),
|
||||
);
|
||||
|
||||
// Import TraceEvaluationsView
|
||||
const TraceEvaluationsView = defineAsyncComponent(
|
||||
() => import("./TraceEvaluationsView.vue"),
|
||||
);
|
||||
|
||||
// Import ThreadView (LLM Thread tab)
|
||||
const ThreadView = defineAsyncComponent(
|
||||
() => import("./ThreadView.vue"),
|
||||
@@ -983,7 +952,6 @@ export default defineComponent({
|
||||
TraceDAG,
|
||||
TraceHeader,
|
||||
FlameGraphView,
|
||||
TraceEvaluationsView,
|
||||
OToggleGroup,
|
||||
OToggleGroupItem,
|
||||
OButton,
|
||||
@@ -1370,13 +1338,6 @@ export default defineComponent({
|
||||
return spans.some((span: any) => isLLMTrace(span));
|
||||
});
|
||||
|
||||
// Evaluation pipeline state
|
||||
const evalPipelineExists = ref(false);
|
||||
const evalPipelineStreamName = ref<string | null>(null);
|
||||
const evalData = ref<any[]>([]);
|
||||
const isLoadingEvalData = ref(false);
|
||||
const traceEvaluationsViewRef = ref<any>(null);
|
||||
|
||||
// Computed properties for new header
|
||||
const errorSpansCount = computed(() => {
|
||||
const spans = effectiveSpanList.value;
|
||||
@@ -1412,14 +1373,6 @@ export default defineComponent({
|
||||
if (hasLLMSpans.value) {
|
||||
tabs.push({ label: "Thread", value: "thread" });
|
||||
}
|
||||
// Conditionally add Evaluations tab for LLM traces with evaluation data
|
||||
if (
|
||||
hasLLMSpans.value &&
|
||||
evalPipelineExists.value &&
|
||||
evalData.value.length > 0
|
||||
) {
|
||||
tabs.push({ label: "Evaluations", value: "evaluations" });
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
|
||||
@@ -1498,37 +1451,6 @@ export default defineComponent({
|
||||
},
|
||||
);
|
||||
|
||||
// Watch for stream and LLM spans to fetch evaluation pipeline
|
||||
watch(
|
||||
() => [currentTraceStreamName.value, hasLLMSpans.value],
|
||||
async ([streamName, hasLlm]) => {
|
||||
if (hasLlm && streamName) {
|
||||
await fetchEvalPipeline();
|
||||
} else {
|
||||
evalPipelineExists.value = false;
|
||||
evalPipelineStreamName.value = null;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Watch for trace ID changes to fetch evaluation data
|
||||
watch(
|
||||
() => [
|
||||
effectiveTraceId.value,
|
||||
evalPipelineExists.value,
|
||||
evalPipelineStreamName.value,
|
||||
],
|
||||
async ([traceId, pipelineExists, streamName]) => {
|
||||
if (pipelineExists && streamName && traceId) {
|
||||
await fetchEvalData();
|
||||
} else {
|
||||
evalData.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const updateActiveTab = (tab: string) => {
|
||||
activeTab.value = tab;
|
||||
if(tab === 'map') {
|
||||
@@ -2657,130 +2579,6 @@ export default defineComponent({
|
||||
await setupTraceDetails();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch LLM evaluation pipeline for the current trace stream
|
||||
*/
|
||||
const fetchEvalPipeline = async () => {
|
||||
if (!currentTraceStreamName.value) {
|
||||
evalPipelineExists.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const orgId = store.state.selectedOrganization.identifier;
|
||||
const res = await pipelineService.getPipelines(orgId);
|
||||
const pipelines: any[] = res.data?.list || [];
|
||||
|
||||
console.log(
|
||||
"[TraceEval] Fetched pipelines:",
|
||||
pipelines.length,
|
||||
"Stream:",
|
||||
currentTraceStreamName.value,
|
||||
);
|
||||
|
||||
// Find pipeline with matching source stream and llm_evaluation node
|
||||
const evalPipeline = pipelines.find(
|
||||
(p: any) =>
|
||||
p.source?.stream_name === currentTraceStreamName.value &&
|
||||
p.source?.stream_type === "traces" &&
|
||||
p.nodes?.some((n: any) => n.data?.node_type === "llm_evaluation"),
|
||||
);
|
||||
|
||||
if (evalPipeline) {
|
||||
evalPipelineExists.value = true;
|
||||
// Get the evaluation output stream name
|
||||
const evalOutputNode = evalPipeline.nodes.find(
|
||||
(n: any) =>
|
||||
n.io_type === "output" && n.data?.stream_type === "logs",
|
||||
);
|
||||
evalPipelineStreamName.value =
|
||||
evalOutputNode?.data?.stream_name ||
|
||||
`${currentTraceStreamName.value}_evaluations`;
|
||||
console.log(
|
||||
"[TraceEval] Found eval pipeline, stream:",
|
||||
evalPipelineStreamName.value,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"[TraceEval] No eval pipeline found for stream:",
|
||||
currentTraceStreamName.value,
|
||||
);
|
||||
evalPipelineExists.value = false;
|
||||
evalPipelineStreamName.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching evaluation pipeline:", error);
|
||||
evalPipelineExists.value = false;
|
||||
evalPipelineStreamName.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch evaluation data for the current trace
|
||||
*/
|
||||
const fetchEvalData = async () => {
|
||||
if (
|
||||
!evalPipelineExists.value ||
|
||||
!evalPipelineStreamName.value ||
|
||||
!effectiveTraceId.value
|
||||
) {
|
||||
evalData.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingEvalData.value = true;
|
||||
try {
|
||||
console.log(
|
||||
"[TraceEval] Fetching data from stream:",
|
||||
evalPipelineStreamName.value,
|
||||
"traceId:",
|
||||
effectiveTraceId.value,
|
||||
);
|
||||
|
||||
const req = {
|
||||
query: {
|
||||
sql: `SELECT * FROM "${evalPipelineStreamName.value}" WHERE trace_id = '${sanitizeTraceId(effectiveTraceId.value)}' ORDER BY _timestamp ASC`,
|
||||
start_time: effectiveTimeRange.value.from - 60000000,
|
||||
end_time: effectiveTimeRange.value.to + 60000000,
|
||||
from: 0,
|
||||
size: 100,
|
||||
},
|
||||
};
|
||||
|
||||
const res = await searchService.search(
|
||||
{
|
||||
org_identifier:
|
||||
(router.currentRoute.value.query?.org_identifier as string) ||
|
||||
store.state.selectedOrganization.identifier,
|
||||
query: req,
|
||||
page_type: "logs",
|
||||
},
|
||||
"ui",
|
||||
);
|
||||
|
||||
evalData.value = res.data?.hits || [];
|
||||
console.log(
|
||||
"[TraceEval] Fetched evaluation records:",
|
||||
evalData.value.length,
|
||||
);
|
||||
|
||||
// Load templates for the current org
|
||||
if (
|
||||
traceEvaluationsViewRef.value?.loadTemplates &&
|
||||
effectiveOrgIdentifier.value
|
||||
) {
|
||||
await traceEvaluationsViewRef.value.loadTemplates(
|
||||
effectiveOrgIdentifier.value,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching evaluation data:", error);
|
||||
evalData.value = [];
|
||||
} finally {
|
||||
isLoadingEvalData.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
router,
|
||||
t,
|
||||
@@ -2905,14 +2703,6 @@ export default defineComponent({
|
||||
// FlameGraph data
|
||||
flatSpans,
|
||||
traceMetadata,
|
||||
// Evaluation data
|
||||
evalPipelineExists,
|
||||
evalPipelineStreamName,
|
||||
evalData,
|
||||
isLoadingEvalData,
|
||||
traceEvaluationsViewRef,
|
||||
fetchEvalPipeline,
|
||||
fetchEvalData,
|
||||
formatLargeNumber,
|
||||
updateActiveTab
|
||||
};
|
||||
|
||||
@@ -358,28 +358,6 @@ describe("Functions.vue", () => {
|
||||
expect((wrapper.vm as any).isDetailView).toBe(false);
|
||||
});
|
||||
|
||||
it("should compute pipelineSections excluding hidden pipelines", async () => {
|
||||
wrapper = await createWrapper(
|
||||
{ zoConfig: { custom_hide_menus: "pipelines" } },
|
||||
"pipelines",
|
||||
);
|
||||
const sections = (wrapper.vm as any).pipelineSections;
|
||||
const streamPipelinesSection = sections.find(
|
||||
(s: any) => s.key === "streamPipelines",
|
||||
);
|
||||
expect(streamPipelinesSection?.visible).toBe(false);
|
||||
});
|
||||
|
||||
it("should compute pipelineSections with streamPipelines present when not hidden", async () => {
|
||||
wrapper = await createWrapper({}, "pipelines");
|
||||
const sections = (wrapper.vm as any).pipelineSections;
|
||||
const streamPipelinesSection = sections.find(
|
||||
(s: any) => s.key === "streamPipelines",
|
||||
);
|
||||
// When not hidden, visible is not explicitly set to false
|
||||
expect(streamPipelinesSection).toBeDefined();
|
||||
expect(streamPipelinesSection?.to?.name).toBe("pipelines");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Store Integration", () => {
|
||||
|
||||
@@ -144,7 +144,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<script lang="ts">
|
||||
import AppPageHeader from "@/components/common/AppPageHeader.vue";
|
||||
import PipelineSectionTabs from "@/components/pipeline/PipelineSectionTabs.vue";
|
||||
import type { SectionHubItem } from "@/components/common/SectionHub.vue";
|
||||
import OButton from "@/lib/core/Button/OButton.vue";
|
||||
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
|
||||
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
|
||||
@@ -193,9 +192,6 @@ export default defineComponent({
|
||||
pipelineBackfill: "streamPipelines",
|
||||
functionList: "functions",
|
||||
enrichmentTables: "enrichmentTables",
|
||||
evalTemplates: "evalTemplates",
|
||||
evalTemplatesAdd: "evalTemplates",
|
||||
evalTemplatesEdit: "evalTemplates",
|
||||
};
|
||||
|
||||
const routeName = computed(() => router.currentRoute.value.name as string);
|
||||
@@ -211,42 +207,6 @@ export default defineComponent({
|
||||
() => store.state.selectedOrganization.identifier,
|
||||
);
|
||||
|
||||
// L2 sections for the switcher dropdown.
|
||||
const pipelineSections = computed<SectionHubItem[]>(() => {
|
||||
const q = { org_identifier: orgIdentifier.value };
|
||||
const hideStreamPipelines = store.state.zoConfig?.custom_hide_menus
|
||||
?.split(",")
|
||||
.includes("pipelines");
|
||||
return [
|
||||
{
|
||||
key: "streamPipelines",
|
||||
label: t("function.streamPipeline"),
|
||||
icon: "lan",
|
||||
to: { name: "pipelines", query: q },
|
||||
visible: !hideStreamPipelines,
|
||||
},
|
||||
{
|
||||
key: "functions",
|
||||
label: t("function.header"),
|
||||
icon: "function",
|
||||
to: { name: "functionList", query: q },
|
||||
},
|
||||
{
|
||||
key: "enrichmentTables",
|
||||
label: t("function.enrichmentTables"),
|
||||
icon: "dataset",
|
||||
to: { name: "enrichmentTables", query: q },
|
||||
},
|
||||
{
|
||||
key: "evalTemplates",
|
||||
label: t("pipeline.evalTemplates"),
|
||||
icon: "fact-check",
|
||||
to: { name: "evalTemplates", query: q },
|
||||
visible: config.isEnterprise == "true",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// ── Level 3 detail crumb ────────────────────────────────────────────────
|
||||
const detailLabels: Record<string, () => string> = {
|
||||
pipelineEditor: () =>
|
||||
@@ -345,7 +305,6 @@ export default defineComponent({
|
||||
config,
|
||||
orgIdentifier,
|
||||
activeTab,
|
||||
pipelineSections,
|
||||
isDetailView,
|
||||
breadcrumbLabel,
|
||||
detailBack,
|
||||
|
||||
Reference in New Issue
Block a user