@@ -78,7 +80,7 @@
mdi-information
@@ -135,7 +137,7 @@
>
- Change Owner
-
+ -->
-
-
+
+
+
+
+
-
@@ -203,13 +207,11 @@
-
-
-
+ Use the template below to execute the pipeline using cloud events
or API calls.
Pipeline name: {{ selectedPipeline.name }}
Change the parameter values as necessary.
+
+
+
{{
JSON.stringify(executionPayload, null, 2)
}}
@@ -295,7 +300,7 @@ import { useAuthStore } from '@/stores/auth';
import { useToolStore } from '@/stores/tools';
import { usePipelineStore } from '@/stores/pipelines';
import { useSettingsStore } from '@/stores/settings';
-import JsonToHtmlTable from '@/components/JsonToHtmlTable.vue';
+import JSONTableViewer from '@/components/JSONTableViewer.vue';
import PipelineCreationPanel from './PipelineCreationPanel.vue';
import PipelineExecutionPanel from './PipelineExecutionPanel.vue';
import { formatDate } from '@/assets/tools';
@@ -304,7 +309,7 @@ export default {
name: 'PipelineList',
components: {
- JsonToHtmlTable,
+ JSONTableViewer,
PipelineCreationPanel,
PipelineExecutionPanel,
},
@@ -317,7 +322,7 @@ export default {
selectedPipeline: null,
creationPanelVisible: false,
creationParameters: {},
- pipelineSuccessMessage: null,
+ successMessage: null,
editionPanelVisible: false,
editionParameters: {},
executionPanelVisible: false,
@@ -453,6 +458,7 @@ export default {
'edited_at',
'tools',
'default_inputs',
+ 'quality_rules',
];
if (this.authStore.isAdmin) {
// Only display these properties to admin users
@@ -475,10 +481,10 @@ export default {
},
viewPipelineExecutions(pipeline) {
- // Store the selected pipeline in the store so it accessible by the executions page
+ // Store the selected pipeline in the store so it is accessible by the executions page
// Navigate to the executions page
this.pipelineStore.selectedPipelineId = pipeline.id;
- this.$router.push('/executions');
+ this.$router.push('/executions?pipeline=' + pipeline.id);
},
createPipeline() {
@@ -577,12 +583,6 @@ export default {
return pipeline_params;
},
- openDeleteDialog(pipeline) {
- console.log('Delete pipeline:', pipeline);
- this.selectedPipeline = pipeline;
- this.showDeleteDialog = true;
- },
-
showExecutionPanel(pipeline) {
console.log('Selected pipeline:', pipeline);
this.refreshTools();
@@ -618,7 +618,7 @@ export default {
},
deletePipeline(pipeline) {
- console.log('Delete pipeline ...');
+ console.log('Delete pipeline:', pipeline);
this.selectedPipeline = pipeline;
this.showDeleteDialog = true;
},
@@ -650,7 +650,7 @@ export default {
handleExecutionSubmitted(execution) {
// Handle the new execution
console.log('New execution created:', execution);
- // Navigate to the Monitoring page
+ // Navigate to the Executions page
this.viewPipelineExecutions(execution.pipeline);
// Show a success message
this.$notify({
diff --git a/frontend/src/components/ReportList.vue b/frontend/src/components/ReportList.vue
index 81a048c..5a72532 100644
--- a/frontend/src/components/ReportList.vue
+++ b/frontend/src/components/ReportList.vue
@@ -64,6 +64,7 @@
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ |
-
-
+
+
-
-
+
+
+
+
+
- {{
- JSON.stringify(selectedReport, null, 2)
- }}
@@ -178,11 +219,14 @@
+
+
diff --git a/frontend/src/components/TriggerList.vue b/frontend/src/components/TriggerList.vue
new file mode 100644
index 0000000..a5dc3aa
--- /dev/null
+++ b/frontend/src/components/TriggerList.vue
@@ -0,0 +1,602 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ triggerStore.listError }}
+
+
+
+
+
+
+
+ |
+
+ {{ item.trigger_type_name }}
+ -->
+ {{ item.pipeline_name }}
+
+ {{ item.description }}
+ |
+ {{ item.owner?.full_name }} |
+
+
+ {{ item.status }} |
+
+
+
+
+ {{ item.enabled ? 'Yes' : 'No' }}
+
+
+ |
+ {{ item.event_count }} |
+
+
+ mdi-information
+
+
+ mdi-monitor-eye
+
+
+
+
+
+ mdi-dots-vertical
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete the trigger definition "{{ selectedTrigger?.slug }}"?
+
+ {{ selectedTrigger?.description }}
+
+
+ This action cannot be undone.
+
+
+
+
+
+ Cancel
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 8e7e5e6..e57c278 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -12,6 +12,8 @@ import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import '@mdi/font/css/materialdesignicons.css';
import VueNotify from 'vue3-notify';
+// Docs: https://github.com/bestkolobok/vue3-jsoneditor
+import {JsonEditorPlugin} from 'vue3-ts-jsoneditor';
const vuetify = createVuetify({
components,
@@ -24,5 +26,14 @@ app.use(createPinia());
app.use(router);
app.use(vuetify);
app.use(VueNotify);
+app.use(JsonEditorPlugin, {
+ componentName: 'JsonEditor',
+ options: {
+ // Global JSON Editor options
+ tabSize: 2,
+ indentation: 2,
+ fullWidthButton: false,
+ }
+});
app.mount('#app');
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index f099d09..3991e16 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -4,6 +4,7 @@ import ToolsView from '../views/ToolsView.vue';
import PipelinesView from '../views/PipelinesView.vue';
import ExecutionsView from '../views/ExecutionsView.vue';
import ReportsView from '../views/ReportsView.vue';
+import TriggersView from '../views/TriggersView.vue';
import SettingsView from '../views/SettingsView.vue';
const router = createRouter({
@@ -42,6 +43,11 @@ const router = createRouter({
name: 'reports',
component: ReportsView,
},
+ {
+ path: '/triggers',
+ name: 'triggers',
+ component: TriggersView,
+ },
{
path: '/settings',
name: 'settings',
diff --git a/frontend/src/services/pipelines.js b/frontend/src/services/pipelines.js
index b0d2470..eb26fe1 100644
--- a/frontend/src/services/pipelines.js
+++ b/frontend/src/services/pipelines.js
@@ -37,7 +37,7 @@ export const pipelineService = {
async getPipelineById(pipelineId) {
try {
- const response = await pipelineApi.get(`/${pipelineId}`);
+ const response = await pipelineApi.get(`/${pipelineId}/`);
return response.data;
} catch (error) {
console.error(`Error fetching pipeline ${pipelineId}:`, error);
@@ -101,7 +101,7 @@ export const pipelineService = {
async getPipelineExecutionById(pipelineId, runId) {
try {
- const response = await pipelineApi.get(`/${pipelineId}/runs/${runId}`);
+ const response = await pipelineApi.get(`/${pipelineId}/runs/${runId}/`);
return response.data;
} catch (error) {
console.error(
@@ -112,6 +112,25 @@ export const pipelineService = {
}
},
+ async setPipelineExecutionQualityStatus(pipelineId, runId, status) {
+ try {
+ const response = await pipelineApi.patch(`/${pipelineId}/runs/${runId}/`,
+ {
+ digest: {
+ digest_quality: status
+ }
+ }
+ );
+ return response.data;
+ } catch (error) {
+ console.error(
+ `Error setting pipeline execution quality status ${pipelineId} ${runId}:`,
+ error,
+ );
+ throw error;
+ }
+ },
+
async getPipelineExecutionReports(pipelineId, runId) {
try {
const response = await pipelineApi.get(
diff --git a/frontend/src/services/triggers.js b/frontend/src/services/triggers.js
new file mode 100644
index 0000000..b961388
--- /dev/null
+++ b/frontend/src/services/triggers.js
@@ -0,0 +1,136 @@
+import axios from 'axios';
+
+const API_URL = '/api';
+const API_TIMEOUT = 5000;
+
+const triggerTypeApi = axios.create({
+ baseURL: API_URL + '/triggertypes',
+ timeout: API_TIMEOUT,
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+});
+
+const triggerApi = axios.create({
+ baseURL: API_URL + '/triggers',
+ timeout: API_TIMEOUT,
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+});
+
+const triggerEventApi = axios.create({
+ baseURL: API_URL + '/triggers/-/events',
+ timeout: API_TIMEOUT,
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+});
+
+export const triggerService = {
+
+ async getTriggerTypes() {
+ try {
+ const response = await triggerTypeApi.get('/');
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching trigger types:', error);
+ throw error;
+ }
+ },
+
+ async getTriggers() {
+ try {
+ const response = await triggerApi.get('/');
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching triggers:', error);
+ throw error;
+ }
+ },
+
+ async getTriggerById(triggerId) {
+ try {
+ const response = await triggerApi.get(`/${triggerId}/`);
+ return response.data;
+ } catch (error) {
+ console.error(`Error fetching trigger ${triggerId}:`, error);
+ throw error;
+ }
+ },
+
+ async createTrigger(trigger) {
+ try {
+ console.log('Create trigger data:', trigger);
+ const response = await triggerApi.post(`/`, trigger);
+ return response.data;
+ } catch (error) {
+ console.error(`Error creating trigger ${trigger.slug}:`, error);
+ throw error;
+ }
+ },
+
+ async updateTrigger(trigger) {
+ try {
+ console.log('Update trigger data:', trigger);
+ const response = await triggerApi.put(`/${trigger.slug}/`, trigger);
+ return response.data;
+ } catch (error) {
+ console.error(
+ // `Error updating trigger ${trigger.name} (Id: ${trigger.slug}):`,
+ `Error updating trigger ${trigger.slug}:`,
+ error,
+ );
+ throw error;
+ }
+ },
+
+ async getTriggerEvents(trigger) {
+ const triggerId = trigger?.slug || trigger;
+ try {
+ const response = await triggerApi.get(`/${triggerId}/events/`);
+ return response.data;
+ } catch (error) {
+ console.error(`Error fetching events for trigger ${triggerId}:`, error);
+ throw error;
+ }
+ },
+
+ async getTriggerEventById(eventId) {
+ try {
+ const response = await triggerEventApi.get(`/${eventId}/`);
+ return response.data;
+ } catch (error) {
+ console.error(`Error fetching trigger event with Id ${eventId}:`, error);
+ throw error;
+ }
+ },
+
+ async getPipelineExecutions(trigger) {
+ const triggerId = trigger?.slug || trigger;
+ try {
+ const response = await triggerApi.get(`/${triggerId}/runs/`);
+ return response.data;
+ } catch (error) {
+ console.error(`Error fetching pipeline runs for trigger ${triggerId}:`, error);
+ throw error;
+ }
+ },
+
+ async deleteTrigger(trigger) {
+ const triggerId = trigger?.slug || trigger;
+ try {
+ console.log('Delete trigger:', triggerId);
+ // Note: In the backend, the trigger status is changed to "Deleted".
+ // Otherwise all the associated events and runs are delected as well due to cascading.
+ const response = await triggerApi.delete(`/${triggerId}/`);
+ return response?.data;
+ } catch (error) {
+ console.error(`Error deleting trigger ${triggerId}:`, error);
+ throw error;
+ }
+ },
+};
diff --git a/frontend/src/stores/pipelines.js b/frontend/src/stores/pipelines.js
index c9d650a..3348b35 100644
--- a/frontend/src/stores/pipelines.js
+++ b/frontend/src/stores/pipelines.js
@@ -239,5 +239,15 @@ export const usePipelineStore = defineStore('pipeline', {
this.loadingPipelines = false;
}
},
+
+ async setExecutionQualityStatus(execution, status) {
+ console.log('Set execution digest quality status:', execution.id, status);
+ // The response contains the updated pipeline run object
+ return await pipelineService.setPipelineExecutionQualityStatus(
+ execution.pipeline,
+ execution.id,
+ status
+ );
+ },
},
});
diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js
index c16c4ad..ba38621 100644
--- a/frontend/src/stores/settings.js
+++ b/frontend/src/stores/settings.js
@@ -27,6 +27,7 @@ export const useSettingsStore = defineStore('settings', {
grafana__pipeline_execution_report__path:
'/d/default-{tool_name}-report/{tool_name}-report?var-pipeline_id={pipeline_name}&var-execution_id={execution_id}&var-report_id={report_id}&var-tool_name={tool_name}',
settingsReceived: false,
+ showDeletedTriggers: false,
}),
actions: {
diff --git a/frontend/src/stores/triggers.js b/frontend/src/stores/triggers.js
new file mode 100644
index 0000000..07b5688
--- /dev/null
+++ b/frontend/src/stores/triggers.js
@@ -0,0 +1,250 @@
+import { defineStore } from 'pinia';
+import { triggerService } from '@/services/triggers';
+import { usePipelineStore } from '@/stores/pipelines';
+
+export const useTriggerStore = defineStore('trigger', {
+ state: () => ({
+ triggerTypes: [],
+ triggers: [],
+ triggerEvents: [],
+ loadingTriggers: false,
+ loadingExecutions: true,
+ selectedTriggerId: null,
+ listError: null, // Use to display errors in the main page
+ error: null, // Used to display errors in the creation/editing panel
+ }),
+
+ actions: {
+ async fetchTriggers() {
+ this.loadingTriggers = true;
+ this.listError = null;
+ try {
+ this.triggerTypes = await triggerService.getTriggerTypes();
+ this.triggers = await triggerService.getTriggers();
+ } catch (error) {
+ const msg_prefix = 'Error fetching triggers: ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.listError = msg_prefix + error.response.data.detail;
+ } else {
+ console.error(msg_prefix, error);
+ this.listError = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingTriggers = false;
+ }
+ },
+
+ async fetchTriggerById(id) {
+ this.loadingTriggers = true;
+ this.listError = null;
+ try {
+ const trigger = await triggerService.getTriggerById(id);
+ const index = this.triggers.findIndex((p) => p.slug === id);
+ if (index !== -1) {
+ this.triggers[index] = trigger;
+ } else {
+ this.triggers.push(trigger);
+ }
+ } catch (error) {
+ const msg_prefix = 'Error fetching trigger: ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.listError = msg_prefix + error.response.data.detail;
+ } else {
+ console.error(msg_prefix, error);
+ this.listError = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingTriggers = false;
+ }
+ },
+
+ // async getTriggerById(id) {
+ getTriggerById(id) {
+ var index = this.triggers.findIndex((p) => p.slug === id);
+ if (index == -1) {
+ // console.log("Trigger not found in store => Fetching it", id)
+ // await this.fetchTriggerById(id)
+ this.fetchTriggerById(id);
+ }
+ index = this.triggers.findIndex((p) => p.slug === id);
+ if (index !== -1) {
+ return this.triggers[index];
+ }
+ return null;
+ },
+
+ async fetchTriggerEventById(id) {
+ this.loadingTriggers = true;
+ this.listError = null;
+ try {
+ const trigger_event = await triggerService.getTriggerEventById(id);
+ const index = this.triggerEvents.findIndex((p) => p.id === id);
+ if (index !== -1) {
+ this.triggerEvents[index] = trigger_event;
+ } else {
+ this.triggerEvents.push(trigger_event);
+ }
+ } catch (error) {
+ const msg_prefix = 'Error fetching trigger event: ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.listError = msg_prefix + error.response.data.detail;
+ } else {
+ console.error(msg_prefix, error);
+ this.listError = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingTriggers = false;
+ }
+ },
+
+ getTriggerEventById(id) {
+ var index = this.triggerEvents.findIndex((p) => p.id === id);
+ if (index == -1) {
+ this.fetchTriggerEventById(id);
+ }
+ index = this.triggerEvents.findIndex((p) => p.id === id);
+ if (index !== -1) {
+ return this.triggerEvents[index];
+ }
+ return null;
+ },
+
+ getTriggerName(id) {
+ const trigger = this.getTriggerById(id);
+ return trigger ? trigger.name : null;
+ },
+
+ getTriggerUserParams(id) {
+ const trigger = this.getTriggerById(id);
+ return trigger ? trigger.user_params : null;
+ },
+
+ hasTriggerUserParams(id) {
+ const trigger = this.getTriggerById(id);
+ return trigger ? Object.keys(trigger.user_params).length !== 0 : false;
+ },
+
+ getTriggerTypeById(id) {
+ var index = this.triggerTypes.findIndex((p) => p.slug === id);
+ if (index == -1) {
+ // console.log("Trigger type not found in store => Fetching it", id)
+ // Note: there is not such "fetchTriggerTypeById(id)" function.
+ // await this.fetchTriggerTypeById(id)
+ //this.fetchTriggerTypeById(id);
+ this.fetchTriggers();
+ }
+ index = this.triggerTypes.findIndex((p) => p.slug === id);
+ if (index !== -1) {
+ return this.triggerTypes[index];
+ }
+ return null;
+ },
+
+ async createTrigger(trigger) {
+ console.log('Create trigger:', trigger.slug, trigger);
+ this.loadingTriggers = true;
+ this.error = null;
+ try {
+ const response = await triggerService.createTrigger(trigger);
+ this.fetchTriggers();
+ this.loadingTriggers = false;
+ return response;
+ } catch (error) {
+ const msg_prefix = 'Error creating trigger ' + trigger.slug + ': ';
+ if (error.response?.data?.detail) {
+ console.error('1' + msg_prefix, error.response.data.detail);
+ this.error = msg_prefix + error.response.data.detail;
+ } else if (error.response?.data) {
+ console.error('2' + msg_prefix, error.response.data);
+ const text = Object.entries(error.response.data)
+ .map(([key, messages]) => `- ${key}: ${messages.join(', ')}`)
+ .join('\n');
+ this.error = msg_prefix + '\n' + text;
+ } else {
+ console.error('3' + msg_prefix, error);
+ this.error = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingTriggers = false;
+ }
+ },
+
+ async updateTrigger(trigger) {
+ console.log('Update trigger:', trigger.slug, trigger);
+ this.loadingTriggers = true;
+ this.error = null;
+ try {
+ const response = await triggerService.updateTrigger(trigger);
+ this.fetchTriggers();
+ this.loadingTriggers = false;
+ return response;
+ } catch (error) {
+ const msg_prefix = 'Error updating trigger ' + trigger.slug + ': ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.error = msg_prefix + error.response.data.detail;
+ } else if (error.response?.data) {
+ console.error('2' + msg_prefix, error.response.data);
+ const text = Object.entries(error.response.data)
+ .map(([key, messages]) => `- ${key}: ${messages.join(', ')}`)
+ .join('\n');
+ this.error = msg_prefix + '\n' + text;
+ } else {
+ console.error(msg_prefix, error);
+ this.error = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingTriggers = false;
+ }
+ },
+
+ async fetchPipelineExecutions(id) {
+ // This is a 2-steps functions: 1/ fetch the events and 2/ fetch the pipeline runs
+ this.loadingExecutions = true;
+ this.listError = null;
+ if (id == undefined) {
+ id = this.selectedTriggerId;
+ }
+ try {
+ const pipelineStore = usePipelineStore()
+ pipelineStore.executions = await triggerService.getPipelineExecutions(id);
+ // console.log('Executions:', this.executions);
+ } catch (error) {
+ const msg_prefix = 'Error fetching pipeline executions: ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.listError = msg_prefix + error.response.data.detail;
+ } else {
+ console.error(msg_prefix, error);
+ this.listError = msg_prefix + error.message;
+ }
+ } finally {
+ this.loadingExecutions = false;
+ }
+ },
+
+ async deleteTrigger(id) {
+ console.log('Delete trigger', id);
+ this.loadingTriggers = true;
+ this.error = null;
+ try {
+ await triggerService.deleteTrigger(id);
+ } catch (error) {
+ const msg_prefix = 'Error deleting trigger ' + id + ': ';
+ if (error.response?.data?.detail) {
+ console.error(msg_prefix, error, error.response.data.detail);
+ this.error = msg_prefix + error.response.data.detail;
+ } else {
+ console.error(msg_prefix, error);
+ this.error = msg_prefix + error.message;
+ }
+ } finally {
+ this.fetchTriggers();
+ this.loadingTriggers = false;
+ }
+ },
+ },
+});
diff --git a/frontend/src/views/ExecutionsView.vue b/frontend/src/views/ExecutionsView.vue
index dd98390..c7372a4 100644
--- a/frontend/src/views/ExecutionsView.vue
+++ b/frontend/src/views/ExecutionsView.vue
@@ -2,7 +2,7 @@
- Analysis Pipeline Executions
+ Analysis Pipelines Executions
diff --git a/frontend/src/views/PipelinesView.vue b/frontend/src/views/PipelinesView.vue
index fe209bb..afda1a0 100644
--- a/frontend/src/views/PipelinesView.vue
+++ b/frontend/src/views/PipelinesView.vue
@@ -2,7 +2,7 @@
- Application Quality Pipelines
+ Analysis Pipelines
diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue
index 27be1ee..8c8a649 100644
--- a/frontend/src/views/SettingsView.vue
+++ b/frontend/src/views/SettingsView.vue
@@ -39,18 +39,29 @@
>Grafana Dashboards Pipeline execution report 35.
+
diff --git a/frontend/src/views/ToolsView.vue b/frontend/src/views/ToolsView.vue
index c444014..05e8996 100644
--- a/frontend/src/views/ToolsView.vue
+++ b/frontend/src/views/ToolsView.vue
@@ -2,7 +2,7 @@
- Application Quality Analysis Tools
+ Analysis Tools
diff --git a/frontend/src/views/TriggersView.vue b/frontend/src/views/TriggersView.vue
new file mode 100644
index 0000000..18c841e
--- /dev/null
+++ b/frontend/src/views/TriggersView.vue
@@ -0,0 +1,20 @@
+
+
+
+
+ Analysis Pipelines Triggers
+
+
+
+
+
+
|