This is an automated email from the ASF dual-hosted git repository.
tenthe pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new f5c2367633 1357 harmonize resource creation workflow for pipelines
(#4708)
f5c2367633 is described below
commit f5c23676336a67df882162a2c949e1b37d56fcb4
Author: Jacqueline Höllig <[email protected]>
AuthorDate: Thu Jul 23 18:08:03 2026 +0200
1357 harmonize resource creation workflow for pipelines (#4708)
Co-authored-by: Philipp Zehnder <[email protected]>
---
ui/cypress/support/utils/pipeline/PipelineBtns.ts | 39 +-
ui/cypress/support/utils/pipeline/PipelineUtils.ts | 65 ++-
.../editAdapterSettingsAndPipeline.smoke.spec.ts | 4 +-
ui/cypress/tests/pipeline/pipelineAsset.spec.ts | 58 +--
.../pipeline/pipelineDatasetSchemaUpdate.spec.ts | 6 +-
.../tests/pipeline/renamePipelineTest.spec.ts | 19 +-
.../pipeline/updatePipelineTest.smoke.spec.ts | 14 +-
ui/deployment/i18n/de.json | 22 +-
ui/deployment/i18n/en.json | 14 +-
ui/deployment/i18n/pl.json | 20 +-
.../pipeline-started-status.component.html | 2 +-
.../pipeline-assembly-options.component.html | 51 ++-
.../pipeline-assembly-options.component.ts | 42 +-
.../pipeline-assembly.component.html | 5 +-
.../pipeline-assembly.component.ts | 312 +++++++++++++-
.../save-pipeline-settings.component.html | 177 --------
.../save-pipeline-settings.component.scss | 21 -
.../save-pipeline-settings.component.ts | 181 --------
.../save-pipeline/save-pipeline.component.html | 118 ++---
.../save-pipeline/save-pipeline.component.scss | 18 -
.../save-pipeline/save-pipeline.component.ts | 476 +++++++--------------
ui/src/app/editor/editor.component.html | 1 +
ui/src/app/editor/editor.component.ts | 5 +-
.../pipeline-overview.component.html | 26 +-
.../services/pipeline-operations.service.ts | 127 +++++-
25 files changed, 884 insertions(+), 939 deletions(-)
diff --git a/ui/cypress/support/utils/pipeline/PipelineBtns.ts
b/ui/cypress/support/utils/pipeline/PipelineBtns.ts
index 833c1b170b..70c4bbd2f6 100644
--- a/ui/cypress/support/utils/pipeline/PipelineBtns.ts
+++ b/ui/cypress/support/utils/pipeline/PipelineBtns.ts
@@ -32,6 +32,22 @@ export class PipelineBtns {
return cy.dataCy('modify-pipeline-btn');
}
+ public static clonePipeline() {
+ return cy.dataCy('clone-pipeline-btn');
+ }
+
+ public static managePipeline() {
+ return cy.dataCy('open-manage-pipeline');
+ }
+
+ public static pipelineOptions() {
+ return cy.dataCy('options-pipeline');
+ }
+
+ public static managePipelineInEditor() {
+ return cy.dataCy('manage-pipeline-btn');
+ }
+
public static pipelinesToEditor() {
return cy.dataCy('pipelines-navigate-to-editor');
}
@@ -83,27 +99,35 @@ export class PipelineBtns {
}
public static savePipelineBtn() {
- return cy.dataCy('sp-editor-save-pipeline');
+ return cy
+ .get('sp-split-button[datacy="sp-editor-save-pipeline"]', {
+ timeout: 15000,
+ })
+ .find('.split-button__main');
}
public static pipelineCloneModeBtn() {
return cy.dataCy('pipeline-update-mode-clone');
}
- public static navigateToOverviewCheckbox() {
- return cy.dataCy('sp-editor-checkbox-navigate-to-overview');
+ public static managedResourceName() {
+ return cy.dataCy('managed-resource-name');
}
public static editorApplyBtn() {
return cy.dataCy('sp-editor-apply');
}
+ public static editorSaveBtn() {
+ return cy.dataCy('sp-manage-save');
+ }
+
public static pipelineStartedError() {
return cy.dataCy('sp-pipeline-started-error', { timeout: 15000 });
}
public static pipelineStartedSuccess() {
- return cy.dataCy('sp-pipeline-started-success', { timeout: 15000 });
+ return cy.dataCy('sp-pipeline-started', { timeout: 15000 });
}
public static updateAndMigratePipeline() {
@@ -159,11 +183,14 @@ export class PipelineBtns {
return cy.dataCy('settings-pipeline-element-button');
}
public static pipelineEditorSave() {
- return cy.dataCy('sp-editor-save-pipeline');
+ return PipelineBtns.savePipelineBtn();
}
+ public static savePipelineStatusClose() {
+ return cy.dataCy('sp-save-pipeline-status-close');
+ }
public static pipelineAssetCheckbox() {
- return cy.dataCy('sp-show-pipeline-asset-checkbox');
+ return cy.dataCy('sp-show-asset-checkbox');
}
public static pipelineEditorCancel() {
diff --git a/ui/cypress/support/utils/pipeline/PipelineUtils.ts
b/ui/cypress/support/utils/pipeline/PipelineUtils.ts
index a5b72746c0..6d4b65bab2 100644
--- a/ui/cypress/support/utils/pipeline/PipelineUtils.ts
+++ b/ui/cypress/support/utils/pipeline/PipelineUtils.ts
@@ -157,9 +157,7 @@ export class PipelineUtils {
// Save and start pipeline
PipelineBtns.savePipelineBtn().click();
if (pipelineInput) {
- cy.dataCy('sp-editor-pipeline-name').type(
- pipelineInput.pipelineName,
- );
+
PipelineBtns.managedResourceName().type(pipelineInput.pipelineName);
}
PipelineUtils.finalizePipelineStart();
}
@@ -171,15 +169,13 @@ export class PipelineUtils {
// Save and start pipeline
PipelineBtns.savePipelineBtn().click();
if (pipelineInput) {
- cy.dataCy('sp-editor-pipeline-name').type(
- pipelineInput.pipelineName,
- );
+
PipelineBtns.managedResourceName().type(pipelineInput.pipelineName);
}
PipelineUtils.finalizePipelineStart(assetNameList);
}
private static addToAsset(assetNameList) {
- cy.dataCy('sp-show-pipeline-asset-checkbox')
+ PipelineBtns.pipelineAssetCheckbox()
.find('input[type="checkbox"]')
.then($checkbox => {
if (!$checkbox.prop('checked')) {
@@ -197,27 +193,64 @@ export class PipelineUtils {
});
}
- public static clonePipeline(newPipelineName: string) {
- cy.dataCy('sp-editor-pipeline-name').type(newPipelineName);
- PipelineBtns.pipelineCloneModeBtn().children().click();
+ public static addManagedPipelineToAssets(assetNameList: string[]) {
+ PipelineUtils.addToAsset(assetNameList);
+ }
+
+ public static clonePipeline(
+ pipelineName: string,
+ newPipelineName?: string,
+ ) {
+ GeneralUtils.openMenuForRow(pipelineName);
+ PipelineBtns.clonePipeline().first().click();
+ PipelineBtns.savePipelineBtn().should('be.visible').click();
+
+ if (newPipelineName) {
+ PipelineBtns.managedResourceName()
+ .should('be.visible')
+ .clear()
+ .type(newPipelineName);
+ }
}
public static updatePipeline(newPipelineName: string) {
- //PipelineBtns.pipelineCloneModeBtn().children().click();
- cy.dataCy('sp-editor-pipeline-name').type(newPipelineName);
+ PipelineBtns.managedResourceName().type(newPipelineName);
+ }
+
+ public static openPipelineManagementInEditor() {
+ PipelineBtns.pipelineOptions().click();
+ PipelineBtns.managePipelineInEditor().click();
+ }
+
+ public static renameManagedPipeline(newPipelineName: string) {
+ PipelineBtns.managedResourceName().clear();
+ PipelineUtils.updatePipeline(newPipelineName);
+ }
+
+ public static applyPipelineManagementChanges() {
+ PipelineBtns.editorSaveBtn().click();
}
public static finalizePipelineStart(assetNameList?: string[]) {
- PipelineBtns.navigateToOverviewCheckbox().children().click();
if (assetNameList) {
PipelineUtils.addToAsset(assetNameList);
}
- PipelineBtns.editorApplyBtn().click();
- cy.dataCy('sp-pipeline-started-success', { timeout: 15000 }).should(
+ PipelineUtils.applyPipelineManagementChanges();
+ PipelineUtils.closePipelineSaveStatus();
+ }
+
+ public static savePipelineUpdate() {
+ PipelineBtns.savePipelineBtn().click();
+ PipelineUtils.closePipelineSaveStatus();
+ PipelineBtns.pipelinesToEditor().should('exist');
+ }
+
+ private static closePipelineSaveStatus() {
+ cy.dataCy('sp-pipeline-started', { timeout: 15000 }).should(
'be.visible',
);
- PipelineBtns.navigateToPipelineOverview().click();
+ PipelineBtns.savePipelineStatusClose().click();
}
public static checkAmountOfPipelinesPipeline(amount: number) {
diff --git
a/ui/cypress/tests/connect/editAdapterSettingsAndPipeline.smoke.spec.ts
b/ui/cypress/tests/connect/editAdapterSettingsAndPipeline.smoke.spec.ts
index b1ea22176a..40d5231981 100644
--- a/ui/cypress/tests/connect/editAdapterSettingsAndPipeline.smoke.spec.ts
+++ b/ui/cypress/tests/connect/editAdapterSettingsAndPipeline.smoke.spec.ts
@@ -99,9 +99,7 @@ describe('Test Edit Adapter and Pipeline', () => {
.click({ force: true });
PipelineBtns.saveElementConfigBtn().click({ force: true });
PipelineBtns.savePipelineBtn().click();
- PipelineBtns.navigateToOverviewCheckbox().children().click();
- PipelineBtns.editorApplyBtn().click();
- PipelineBtns.navigateToPipelineOverview().click();
+ PipelineBtns.savePipelineStatusClose().click();
// Visit dashboard
cy.wait(5000);
diff --git a/ui/cypress/tests/pipeline/pipelineAsset.spec.ts
b/ui/cypress/tests/pipeline/pipelineAsset.spec.ts
index f9aa31fb11..31b45d7913 100644
--- a/ui/cypress/tests/pipeline/pipelineAsset.spec.ts
+++ b/ui/cypress/tests/pipeline/pipelineAsset.spec.ts
@@ -27,6 +27,10 @@ describe('Test Saving Pipeline with Asset Link', () => {
const assetName1 = 'Test1';
const assetName2 = 'Test2';
const assetName3 = 'Test3';
+ const initialPipelineName = 'Pipeline Test';
+ const renamedPipelineName = 'Renamed Pipeline';
+ const linkedPipelineResources = 1;
+
beforeEach('Setup Test', () => {
cy.initStreamPipesTest();
AssetUtils.goToAssets();
@@ -42,7 +46,7 @@ describe('Test Saving Pipeline with Asset Link', () => {
ConnectUtils.addMachineDataSimulator(adapterName);
- const pipelineInput = PipelineBuilder.create('Pipeline Test')
+ const pipelineInput = PipelineBuilder.create(initialPipelineName)
.addSource(adapterName)
.addSink(
PipelineElementBuilder.create('data_lake')
@@ -55,44 +59,44 @@ describe('Test Saving Pipeline with Asset Link', () => {
assetName1,
assetName2,
]);
+ PipelineUtils.goToPipelines();
});
it('Add Pipeline to Asset during creation', () => {
- PipelineUtils.editPipeline('Pipeline Test');
-
- // Go Back to Asset
AssetUtils.goToAssets();
- AssetUtils.checkAmountOfAssetsGreaterThan(0);
-
- // CLick on Asset
+ AssetUtils.checkAmountOfLinkedResourcesByAssetName(
+ assetName1,
+ linkedPipelineResources,
+ );
+ AssetUtils.checkAmountOfLinkedResourcesByAssetName(
+ assetName2,
+ linkedPipelineResources,
+ );
+ });
- AssetUtils.editAsset(assetName1);
- AssetUtils.checkAmountOfLinkedResources(2);
+ it('Edit Pipeline to Asset during Edit', () => {
+ PipelineUtils.editPipeline(initialPipelineName);
+ PipelineUtils.openPipelineManagementInEditor();
+ PipelineUtils.renameManagedPipeline(renamedPipelineName);
+ PipelineUtils.addManagedPipelineToAssets([assetName3]);
+ PipelineUtils.applyPipelineManagementChanges();
+ PipelineUtils.savePipelineUpdate();
- // Go Back to Asset
AssetUtils.goToAssets();
- AssetUtils.checkAmountOfAssetsGreaterThan(0);
- AssetUtils.editAsset(assetName2);
- AssetUtils.checkAmountOfLinkedResources(2);
- });
- it('Edit Pipeline to Asset during Edit', () => {
- PipelineUtils.editPipeline('Pipeline Test');
- cy.dataCy('sp-editor-save-pipeline', { timeout: 10000 })
- .should('exist')
- .click();
- cy.dataCy('sp-editor-pipeline-name').clear();
- PipelineUtils.updatePipeline('Renamed Pipeline');
- PipelineUtils.finalizePipelineStart([assetName1, assetName3]);
+ AssetUtils.checkAmountOfLinkedResourcesByAssetName(
+ assetName2,
+ linkedPipelineResources,
+ );
- // Test Number of Asset Links
- AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName2, 2);
- AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName3, 2);
+ AssetUtils.checkAmountOfLinkedResourcesByAssetName(
+ assetName3,
+ linkedPipelineResources,
+ );
- // Test Renaming
AssetUtils.checkResourceNamingByAssetName(
assetName2,
- 'Renamed Pipeline',
+ renamedPipelineName,
);
});
});
diff --git a/ui/cypress/tests/pipeline/pipelineDatasetSchemaUpdate.spec.ts
b/ui/cypress/tests/pipeline/pipelineDatasetSchemaUpdate.spec.ts
index de7a007b0c..9edf87fb0e 100644
--- a/ui/cypress/tests/pipeline/pipelineDatasetSchemaUpdate.spec.ts
+++ b/ui/cypress/tests/pipeline/pipelineDatasetSchemaUpdate.spec.ts
@@ -81,8 +81,7 @@ describe('Test pipeline updates with data lake schema
changes', () => {
PipelineBtns.updateAndMigratePipeline().should('not.be.disabled');
PipelineBtns.updateAndMigratePipeline().click();
- PipelineBtns.pipelineStartedSuccess().should('be.visible');
- PipelineBtns.navigateToPipelineOverview().click();
+ PipelineUtils.closePipelineSaveStatus();
ChartUtils.goToDatalake();
ChartBtns.chartSyncProblemIcon().should('be.visible');
@@ -109,7 +108,6 @@ describe('Test pipeline updates with data lake schema
changes', () => {
PipelineBtns.pipelineEditWarning().should('not.exist');
PipelineBtns.pipelineStartedSuccess().should('be.visible');
- PipelineBtns.navigateToPipelineOverview().click();
ChartUtils.goToDatalake();
ChartBtns.chartSyncProblemIcon().should('not.exist');
@@ -164,8 +162,6 @@ describe('Test pipeline updates with data lake schema
changes', () => {
function savePipeline() {
PipelineBtns.savePipelineBtn().click();
- PipelineBtns.navigateToOverviewCheckbox().children().click();
- PipelineBtns.editorApplyBtn().click();
}
function addTableChart(measurementName: string) {
diff --git a/ui/cypress/tests/pipeline/renamePipelineTest.spec.ts
b/ui/cypress/tests/pipeline/renamePipelineTest.spec.ts
index e57a295947..67acfb952c 100644
--- a/ui/cypress/tests/pipeline/renamePipelineTest.spec.ts
+++ b/ui/cypress/tests/pipeline/renamePipelineTest.spec.ts
@@ -16,6 +16,7 @@
*
*/
+import { GeneralUtils } from '../../support/utils/GeneralUtils';
import { PipelineBtns } from '../../support/utils/pipeline/PipelineBtns';
import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils';
@@ -30,20 +31,20 @@ describe('Test rename of running pipeline', () => {
PipelineUtils.verifyPipelineCount(1);
PipelineUtils.verifyPipelineName('Pipeline Test');
- PipelineUtils.editPipeline('Pipeline Test');
+ GeneralUtils.openMenuForRow('Pipeline Test');
cy.wait(1000);
- PipelineBtns.savePipelineBtn().click();
- cy.dataCy('sp-editor-pipeline-name').clear();
- PipelineUtils.updatePipeline('Renamed Pipeline');
- PipelineUtils.finalizePipelineStart();
+
+ PipelineBtns.managePipeline().click();
+ PipelineUtils.renameManagedPipeline('Renamed Pipeline');
+ PipelineBtns.editorSaveBtn().click();
PipelineUtils.verifyPipelineCount(1);
PipelineUtils.verifyPipelineName('Renamed Pipeline');
- PipelineUtils.editPipeline('Renamed Pipeline');
- PipelineBtns.savePipelineBtn().click();
- cy.dataCy('sp-editor-pipeline-name').clear();
- PipelineUtils.clonePipeline('Cloned Renamed Pipeline');
+ PipelineUtils.clonePipeline(
+ 'Renamed Pipeline',
+ 'Cloned Renamed Pipeline',
+ );
PipelineUtils.finalizePipelineStart();
PipelineUtils.verifyPipelineCount(2);
diff --git a/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
index d31c0d04f5..7bcaeec675 100644
--- a/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
+++ b/ui/cypress/tests/pipeline/updatePipelineTest.smoke.spec.ts
@@ -29,14 +29,16 @@ describe('Test update of running pipeline', () => {
PipelineUtils.addSampleAdapterAndPipeline();
PipelineUtils.editPipeline(pipelineName);
cy.wait(1000);
- PipelineUtils.startPipeline();
+ PipelineBtns.savePipelineBtn().click();
+ PipelineBtns.savePipelineStatusClose().click();
cy.dataCy('more-options', { timeout: 10000 }).should('have.length', 1);
- PipelineUtils.editPipeline(pipelineName);
- cy.wait(1000);
- PipelineBtns.savePipelineBtn().click();
- PipelineUtils.clonePipeline('Pipeline Test 2');
- PipelineUtils.finalizePipelineStart();
+ PipelineUtils.clonePipeline(pipelineName, 'Pipeline Test 2');
+ PipelineBtns.editorSaveBtn().click();
+ cy.dataCy('sp-pipeline-started', { timeout: 15000 }).should(
+ 'be.visible',
+ );
+ PipelineBtns.savePipelineStatusClose().click();
cy.dataCy('more-options', { timeout: 10000 }).should('have.length', 2);
});
});
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 726f1dbeff..76a652ae74 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -57,7 +57,6 @@
"Add Adapter to an existing Asset": "Adapter zu einem bestehenden Asset
hinzufügen",
"Add Filter": "Filter hinzufügen",
"Add Mapping": "Mapping hinzufügen",
- "Add Pipeline to Assets": "Pipeline zu Assets hinzufügen",
"Add additional fields to the asset, e.g., to manage responsibilities":
"Hinzufügen zusätzlicher Felder zum Asset, z. B. zur Verwaltung von
Zuständigkeiten",
"Add all direct children": "Alle direkten Unterknoten hinzufügen",
"Add an additional link that links to your support page": "Fügen Sie einen
zusätzlichen Link hinzu, der zu Ihrer Support-Seite führt",
@@ -184,7 +183,7 @@
"Charts": "Diagramme",
"Check that the uploaded zip file is a valid export": "Prüfen Sie, ob die
hochgeladene Zip-Datei ein gültiger Export ist",
"Checking migrations for adapter {{adapterName}}": "Migrationen für Adapter
{{adapterName}} prüfen",
- "Checking pipeline update": "Überprüfe Pipeline-Update",
+ "Checking pipeline update": "Pipeline-Aktualisierung prüfen",
"Choose a name for your site": "Name des Standorts festlegen",
"Choose existing file": "Vorhandene Datei auswählen",
"Choose target dataset": "Ziel-Dataset auswählen",
@@ -250,7 +249,6 @@
"Create new API key": "Neuen API-Schlüssel erstellen",
"Create new account": "Neuen Account erstellen",
"Create new dataset": "Neues Dataset erstellen",
- "Create new pipeline": "Neue Pipeline erstellen",
"Create new source": "Neue Quelle erstellen",
"Create template": "Vorlage erstellen",
"Create transformation template": "Skriptvorlage erstellen",
@@ -650,6 +648,7 @@
"Manage Chart ": "Diagramm verwalten",
"Manage Dashboard ": "Dashboard verwalten",
"Manage Labels": "Labels verwalten",
+ "Manage Pipeline ": "Pipeline verwalten ",
"Manage Sites": "Standorte verwalten",
"Manage asset links": "Verwalten von Asset-Verknüpfungen",
"Manage chart": "Diagramm verwalten",
@@ -660,6 +659,7 @@
"Manage permissions for adapter ": "Berechtigungen für Adapter verwalten",
"Manage permissions for dataset ": "Berechtigungen für Datensatz verwalten",
"Manage permissions for pipeline element {{name}}": "Berechtigungen für
Pipeline-Element {{name}} verwalten",
+ "Manage pipeline": "Pipeline verwalten",
"Manage roles": "Rollen verwalten",
"Manage site": "Standorte verwalten",
"Manage user groups": "Benutzergruppen verwalten",
@@ -705,7 +705,6 @@
"Name + Value + Percent": "Name + Wert + Prozent",
"Name used for the grouped remaining slices.": "Name, der für die
gruppierten übrigen Segmente verwendet wird.",
"Navigate": "navigieren.",
- "Navigate to pipeline overview afterwards": "Navigieren Sie anschließend zur
Pipeline-Übersicht",
"Nested": "Verschachtelt",
"Network Error": "Netzwerkfehler",
"New": "Neu",
@@ -797,15 +796,9 @@
"Pie": "Kreisdiagramm",
"Pin": "Pin",
"Pipeline": "Pipeline",
- "Pipeline Name": "Name der Pipeline",
"Pipeline as code": "Pipeline als Code",
- "Pipeline description must not have more than 80 characters.": "Die
Pipeline-Beschreibung darf nicht mehr als 80 Zeichen umfassen.",
"Pipeline elements": "Pipeline-Elemente",
"Pipeline health monitoring discovered the following issues:": "Die
Zustandsüberwachung der Pipeline hat folgende Probleme festgestellt:",
- "Pipeline name can only contain letters, numbers, dashes (-), and
underscores (_).": "Pipeline-Namen dürfen nur Buchstaben, Zahlen, Bindestriche
(-) und Unterstriche (_) enthalten.",
- "Pipeline name cannot start or end with a space.": "Der Name der Pipeline
darf nicht mit einem Leerzeichen beginnen oder enden.",
- "Pipeline name is required.": "Der Name der Pipeline ist erforderlich.",
- "Pipeline name must have between 3 and 50 characters.": "Der Name der
Pipeline muss zwischen 3 und 50 Zeichen haben.",
"Pipelines": "Pipelines",
"Places labels inside slices or outside the chart.": "Platziert
Beschriftungen innerhalb der Segmente oder außerhalb des Diagramms.",
"Please change the adapter configuration to fix them.": "Bitte ändern Sie
die Adapterkonfiguration, um sie zu beheben.",
@@ -942,8 +935,8 @@
"Save in a new data lake": "In einem neuen Data Lake speichern",
"Save pipeline": "Pipeline speichern",
"Save template": "Vorlage speichern",
- "Saving metadata": "Speichern von Metadaten",
- "Saving pipeline": "Speichern der Pipeline",
+ "Saving metadata": "Metadaten speichern",
+ "Saving pipeline": "Pipeline speichern",
"Saving pipeline modifications": "Änderungen werden gespeichert",
"Scatter": "Streudiagramm",
"Scattered Line": "Streudiagramm-Linie",
@@ -1020,7 +1013,6 @@
"Show latest time above card": "Neueste Zeit über der Karte anzeigen",
"Show legend": "Legende anzeigen",
"Show only recommended settings": "Nur empfohlene Einstellungen anzeigen",
- "Show pipeline configuration as code": "Pipeline-Konfiguration als Code
anzeigen",
"Show progress label": "Fortschrittsbeschriftung anzeigen",
"Show raw data from your data source.": "Zeigen Sie Rohdaten aus Ihrer
Datenquelle an.",
"Show single data entry.": "Einzelne Dateneinträge anzeigen.",
@@ -1063,12 +1055,11 @@
"Start export process": "Exportvorgang starten",
"Start import process": "Importvorgang starten",
"Start pipeline": "Pipeline starten",
- "Start pipeline immediately": "Pipeline sofort starten",
"Start selected adapters": "Ausgewählte Adapter starten",
"Starting": "Starte",
"Starting adapter": "Adapter wird gestartet",
"Starting adapter {{adapterName}}": "Adapter starten {{adapterName}}",
- "Starting pipeline": "Start der Pipeline",
+ "Starting pipeline": "Pipeline starten",
"Starting pipeline ": "Starte Pipeline",
"State": "Status",
"Status": "Status",
@@ -1083,6 +1074,7 @@
"Stopping pipeline": "Pipeline stoppen",
"Stopping pipeline...": "Pipeline stoppen...",
"Store all events of this source in the internal data store": "Speichern
aller Daten dieser Quelle im internen DataLake",
+ "Store and Start": "Speichern und Starten",
"Store as template": "Als Vorlage speichern",
"Style/Tile server URL": "Stil/Tile-Server URL",
"Success": "Erfolg",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index 073f3b8872..f0b7ebadf8 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -57,7 +57,6 @@
"Add Adapter to an existing Asset": null,
"Add Filter": null,
"Add Mapping": null,
- "Add Pipeline to Assets": null,
"Add additional fields to the asset, e.g., to manage responsibilities": null,
"Add all direct children": null,
"Add an additional link that links to your support page": null,
@@ -250,7 +249,6 @@
"Create new API key": null,
"Create new account": null,
"Create new dataset": null,
- "Create new pipeline": null,
"Create new source": null,
"Create template": null,
"Create transformation template": null,
@@ -650,6 +648,7 @@
"Manage Chart ": null,
"Manage Dashboard ": null,
"Manage Labels": null,
+ "Manage Pipeline ": null,
"Manage Sites": null,
"Manage asset links": null,
"Manage chart": null,
@@ -660,6 +659,7 @@
"Manage permissions for adapter ": null,
"Manage permissions for dataset ": null,
"Manage permissions for pipeline element {{name}}": "Manage permissions for
pipeline element {{name}}",
+ "Manage pipeline": null,
"Manage roles": null,
"Manage site": null,
"Manage user groups": null,
@@ -705,7 +705,6 @@
"Name + Value + Percent": null,
"Name used for the grouped remaining slices.": null,
"Navigate": null,
- "Navigate to pipeline overview afterwards": null,
"Nested": null,
"Network Error": null,
"New": null,
@@ -797,15 +796,9 @@
"Pie": null,
"Pin": null,
"Pipeline": null,
- "Pipeline Name": null,
"Pipeline as code": null,
- "Pipeline description must not have more than 80 characters.": null,
"Pipeline elements": null,
"Pipeline health monitoring discovered the following issues:": null,
- "Pipeline name can only contain letters, numbers, dashes (-), and
underscores (_).": null,
- "Pipeline name cannot start or end with a space.": null,
- "Pipeline name is required.": null,
- "Pipeline name must have between 3 and 50 characters.": null,
"Pipelines": null,
"Places labels inside slices or outside the chart.": null,
"Please change the adapter configuration to fix them.": null,
@@ -1020,7 +1013,6 @@
"Show latest time above card": null,
"Show legend": null,
"Show only recommended settings": null,
- "Show pipeline configuration as code": null,
"Show progress label": null,
"Show raw data from your data source.": null,
"Show single data entry.": null,
@@ -1063,7 +1055,6 @@
"Start export process": null,
"Start import process": null,
"Start pipeline": null,
- "Start pipeline immediately": null,
"Start selected adapters": null,
"Starting": null,
"Starting adapter": null,
@@ -1083,6 +1074,7 @@
"Stopping pipeline": null,
"Stopping pipeline...": null,
"Store all events of this source in the internal data store": null,
+ "Store and Start": null,
"Store as template": null,
"Style/Tile server URL": null,
"Success": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index 0764b3d447..dab204a19c 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -57,7 +57,6 @@
"Add Adapter to an existing Asset": "Dodaj adapter do istniejącego zasobu",
"Add Filter": "Dodaj filtr",
"Add Mapping": "Dodaj mapowanie",
- "Add Pipeline to Assets": "Dodaj strumień do zasobów",
"Add additional fields to the asset, e.g., to manage responsibilities":
"Dodaj dodatkowe pola do zasobu, np. do zarządzania odpowiedzialnościami",
"Add all direct children": "Dodaj wszystkie bezpośrednie elementy potomne",
"Add an additional link that links to your support page": "Dodaj dodatkowy
link do strony wsparcia",
@@ -184,7 +183,7 @@
"Charts": "Wykresy",
"Check that the uploaded zip file is a valid export": "Sprawdź, czy
przesłany plik ZIP jest prawidłowym eksportem",
"Checking migrations for adapter {{adapterName}}": "Sprawdzanie migracji dla
adaptera {{adapterName}}",
- "Checking pipeline update": "Sprawdzanie aktualizacji strumienia",
+ "Checking pipeline update": "Sprawdzanie aktualizacji potoku",
"Choose a name for your site": "Wybierz nazwę dla swojej lokalizacji",
"Choose existing file": "Wybierz istniejący plik",
"Choose target dataset": "Wybierz docelowy zbiór danych",
@@ -250,7 +249,6 @@
"Create new API key": "Utwórz nowy klucz API",
"Create new account": "Utwórz nowe konto",
"Create new dataset": "Utwórz nowy zbiór danych",
- "Create new pipeline": "Utwórz nowy strumień",
"Create new source": "Utwórz nowe źródło",
"Create template": "Utwórz szablon",
"Create transformation template": "Utwórz szablon transformacji",
@@ -650,6 +648,7 @@
"Manage Chart ": "Zarządzaj wykresem",
"Manage Dashboard ": "Zarządzaj pulpitem nawigacyjnym",
"Manage Labels": "Zarządzaj etykietami",
+ "Manage Pipeline ": "Zarządzaj procesem ",
"Manage Sites": "Zarządzaj lokalizacjami",
"Manage asset links": "Zarządzaj linkami zasobu",
"Manage chart": "Zarządzanie wykresem",
@@ -660,6 +659,7 @@
"Manage permissions for adapter ": "Zarządzaj uprawnieniami dla adaptera ",
"Manage permissions for dataset ": "Zarządzaj uprawnieniami dla zbioru
danych ",
"Manage permissions for pipeline element {{name}}": "Zarządzaj uprawnieniami
dla elementu strumienia {{name}}",
+ "Manage pipeline": "Zarządzaj procesem",
"Manage roles": "Zarządzaj rolami",
"Manage site": "Zarządzaj lokalizacjami",
"Manage user groups": "Zarządzaj grupami użytkowników",
@@ -705,7 +705,6 @@
"Name + Value + Percent": "Nazwa + Wartość + Procent",
"Name used for the grouped remaining slices.": "Nazwa używana dla
zgrupowanych pozostałych segmentów.",
"Navigate": "Przejdź",
- "Navigate to pipeline overview afterwards": "Następnie przejdź do przeglądu
strumieni",
"Nested": "Zagnieżdżone",
"Network Error": "Błąd sieci",
"New": "Nowy",
@@ -797,15 +796,9 @@
"Pie": "Wykres kołowy",
"Pin": "Pinezka",
"Pipeline": "strumień",
- "Pipeline Name": "Nazwa strumienia",
"Pipeline as code": "Strumień jako kod",
- "Pipeline description must not have more than 80 characters.": "Opis
strumieni nie może mieć więcej niż 80 znaków.",
"Pipeline elements": "Elementy strumienia",
"Pipeline health monitoring discovered the following issues:":
"Monitorowanie kondycji strumienia wykryło następujące problemy:",
- "Pipeline name can only contain letters, numbers, dashes (-), and
underscores (_).": "Nazwa strumieni może zawierać tylko litery, cyfry, myślniki
(-) i podkreślenia (_).",
- "Pipeline name cannot start or end with a space.": "Nazwa strumienia nie
może zaczynać się ani kończyć spacją.",
- "Pipeline name is required.": "Nazwa strumienia jest wymagana.",
- "Pipeline name must have between 3 and 50 characters.": "Nazwa strumieni
musi mieć od 3 do 50 znaków.",
"Pipelines": "Strumienie",
"Places labels inside slices or outside the chart.": "Umieszcza etykiety
wewnątrz segmentów lub poza wykresem.",
"Please change the adapter configuration to fix them.": "Zmień konfigurację
adaptera, aby je naprawić.",
@@ -943,7 +936,7 @@
"Save pipeline": "Zapisz strumień",
"Save template": "Zapisz szablon",
"Saving metadata": "Zapisywanie metadanych",
- "Saving pipeline": "Zapisywanie strumienia",
+ "Saving pipeline": "Zapisywanie potoku",
"Saving pipeline modifications": "Zapisywanie zmian w strumieniu",
"Scatter": "Punktowy",
"Scattered Line": "Linia punktowa",
@@ -1020,7 +1013,6 @@
"Show latest time above card": "Pokaż najnowszy czas nad kartą",
"Show legend": "Pokaż legendę",
"Show only recommended settings": "Pokaż tylko zalecane ustawienia",
- "Show pipeline configuration as code": "Pokaż konfigurację strumienia jako
kod",
"Show progress label": "Pokaż etykietę postępu",
"Show raw data from your data source.": "Pokaż surowe dane ze źródła.",
"Show single data entry.": "Pokaż pojedynczy wpis danych.",
@@ -1063,12 +1055,11 @@
"Start export process": "Rozpocznij proces eksportu",
"Start import process": "Rozpocznij proces importu",
"Start pipeline": "Uruchom strumień",
- "Start pipeline immediately": "Uruchom strumień od razu",
"Start selected adapters": "Uruchom wybrane adaptery",
"Starting": "Uruchamianie",
"Starting adapter": "Uruchamianie adaptera",
"Starting adapter {{adapterName}}": "Uruchamianie adaptera {{adapterName}}",
- "Starting pipeline": "Uruchamianie strumienia",
+ "Starting pipeline": "Uruchamianie potoku",
"Starting pipeline ": "Uruchamianie strumienia",
"State": "Stan",
"Status": "Status",
@@ -1083,6 +1074,7 @@
"Stopping pipeline": "Zatrzymywanie strumienia",
"Stopping pipeline...": "Zatrzymywanie strumienia...",
"Store all events of this source in the internal data store": "Przechowuj
wszystkie zdarzenia z tego źródła w wewnętrznej bazie danych",
+ "Store and Start": "Zapisz i uruchom",
"Store as template": "Zapisz jako szablon",
"Style/Tile server URL": "URL stylu/serwera kafelków",
"Success": "Sukces",
diff --git
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
index 065c598477..620c74e488 100644
---
a/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
+++
b/ui/src/app/core-ui/pipeline/pipeline-started-status/pipeline-started-status.component.html
@@ -26,7 +26,7 @@
>
<div fxLayout="row">
@if (pipelineOperationStatus()?.success) {
- <mat-icon data-cy="sp-pipeline-started-success" color="accent"
+ <mat-icon data-cy="sp-pipeline-started" color="accent"
>done</mat-icon
>
}
diff --git
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
index a55604f12d..07d588c2b5 100644
---
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
+++
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.html
@@ -17,20 +17,16 @@
-->
<div fxFlex="100" fxLayout="row" fxLayoutAlign="start center">
- <button
- mat-flat-button
- [matTooltip]="'Save pipeline' | translate"
- [matTooltipPosition]="'above'"
+ <sp-split-button
+ dataCy="sp-editor-save-pipeline"
+ [label]="'Store and Start' | translate"
+ icon="save"
+ [actions]="savePipelineActions"
[disabled]="!pipelineValidationService.pipelineValid"
- (click)="savePipelineEmitter.emit()"
- type="submit"
- data-cy="sp-editor-save-pipeline"
- >
- <div fxLayoutAlign="start center" fxLayout="row">
- <i class="material-icons">save</i>
- <span> {{ 'Save' | translate }}</span>
- </div>
- </button>
+ [menuAriaLabel]="'Select save action'"
+ (primaryAction)="emitSavePipeline(true)"
+ (actionSelected)="onSavePipelineActionSelected($event)"
+ ></sp-split-button>
<span class="assembly-options-divider"></span>
<button
mat-flat-button
@@ -88,7 +84,36 @@
#assemblyOptionsPipelineCacheComponent
>
</sp-pipeline-assembly-options-pipeline-cache>
+
<span fxFlex></span>
+ @if (editMode) {
+ <button
+ mat-icon-button
+ [matMenuTriggerFor]="pipelineOptionsMenu"
+ [attr.aria-label]="'Options' | translate"
+ data-cy="options-pipeline"
+ >
+ <mat-icon>more_vert</mat-icon>
+ </button>
+ <mat-menu #pipelineOptionsMenu="matMenu">
+ <button
+ mat-menu-item
+ (click)="managePipelineEmitter.emit()"
+ data-cy="manage-pipeline-btn"
+ >
+ <mat-icon>settings</mat-icon>
+ <span>{{ 'Manage pipeline' | translate }}</span>
+ </button>
+ <button
+ mat-menu-item
+ (click)="deletePipelineEmitter.emit()"
+ data-cy="delete-pipeline-btn"
+ >
+ <mat-icon>clear</mat-icon>
+ <span>{{ 'Delete pipeline' | translate }}</span>
+ </button>
+ </mat-menu>
+ }
<button
mat-icon-button
[matTooltip]="'Clear Assembly Area' | translate"
diff --git
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
index cdeabf855d..44922fabad 100644
---
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
+++
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly-options/pipeline-assembly-options.component.ts
@@ -31,6 +31,8 @@ import {
ConfirmDialogComponent,
DialogService,
PanelType,
+ SpSplitButtonAction,
+ SpSplitButtonComponent,
} from '@streampipes/shared-ui';
import { EditorService } from '../../../services/editor.service';
import { MatDialog } from '@angular/material/dialog';
@@ -51,9 +53,15 @@ import {
LayoutDirective,
} from '@ngbracket/ngx-layout/flex';
import { MatButton, MatIconButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
+import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
+export interface PipelineAssemblySaveOptions {
+ startPipelineAfterStorage: boolean;
+}
+
@Component({
selector: 'sp-pipeline-assembly-options',
templateUrl: './pipeline-assembly-options.component.html',
@@ -63,13 +71,25 @@ import { TranslatePipe } from '@ngx-translate/core';
LayoutDirective,
LayoutAlignDirective,
MatButton,
+ MatIcon,
MatTooltip,
MatIconButton,
+ MatMenuTrigger,
+ MatMenu,
+ MatMenuItem,
PipelineAssemblyOptionsPipelineCacheComponent,
+ SpSplitButtonComponent,
TranslatePipe,
],
})
export class PipelineAssemblyOptionsComponent {
+ savePipelineActions: SpSplitButtonAction[] = [
+ {
+ label: 'Store',
+ action: 'store',
+ icon: 'save',
+ },
+ ];
editorService = inject(EditorService);
pipelineValidationService = inject(PipelineValidationService);
private pipelinePositioningService = inject(PipelinePositioningService);
@@ -91,8 +111,12 @@ export class PipelineAssemblyOptionsComponent {
@Input()
previewModeActive: boolean;
+ @Input()
+ editMode = false;
+
@Output()
- savePipelineEmitter: EventEmitter<void> = new EventEmitter<void>();
+ savePipelineEmitter: EventEmitter<PipelineAssemblySaveOptions> =
+ new EventEmitter<PipelineAssemblySaveOptions>();
@Output()
clearAssemblyEmitter: EventEmitter<void> = new EventEmitter<void>();
@@ -104,6 +128,12 @@ export class PipelineAssemblyOptionsComponent {
displayPipelineTemplateEmitter: EventEmitter<Pipeline> =
new EventEmitter<Pipeline>();
+ @Output()
+ managePipelineEmitter: EventEmitter<void> = new EventEmitter<void>();
+
+ @Output()
+ deletePipelineEmitter: EventEmitter<void> = new EventEmitter<void>();
+
@ViewChild('assemblyOptionsPipelineCacheComponent')
assemblyOptionsCacheComponent:
PipelineAssemblyOptionsPipelineCacheComponent;
@@ -168,6 +198,16 @@ export class PipelineAssemblyOptionsComponent {
);
}
+ emitSavePipeline(startPipelineAfterStorage: boolean): void {
+ this.savePipelineEmitter.emit({
+ startPipelineAfterStorage,
+ });
+ }
+
+ onSavePipelineActionSelected(action: SpSplitButtonAction): void {
+ this.emitSavePipeline(action.action === 'store-and-start');
+ }
+
triggerCacheUpdate(): void {
this.assemblyOptionsCacheComponent.triggerPipelineCacheUpdate();
}
diff --git
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
index 3914c4fc89..5d423ce280 100644
---
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
+++
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.html
@@ -26,9 +26,12 @@
[allElements]="allElements"
[jsplumbBridge]="jsplumbBridge"
[previewModeActive]="previewModeActive"
+ [editMode]="!!originalPipeline"
(clearAssemblyEmitter)="clearAssembly()"
(togglePreviewEmitter)="togglePreview()"
- (savePipelineEmitter)="submit()"
+ (savePipelineEmitter)="submit($event)"
+ (managePipelineEmitter)="managePipeline()"
+ (deletePipelineEmitter)="deletePipeline()"
(displayPipelineTemplateEmitter)="displayPipelineTemplate($event)"
>
</sp-pipeline-assembly-options>
diff --git
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
index 780a3a4bce..ffcdbed5fc 100644
---
a/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
+++
b/ui/src/app/editor/components/pipeline-assembly/pipeline-assembly.component.ts
@@ -19,6 +19,7 @@
import {
AfterViewInit,
Component,
+ EventEmitter,
Input,
OnDestroy,
ViewChild,
@@ -28,31 +29,46 @@ import { JsplumbBridge } from
'../../services/jsplumb-bridge.service';
import { PipelinePositioningService } from
'../../services/pipeline-positioning.service';
import { PipelineValidationService } from
'../../services/pipeline-validation.service';
import {
+ InvocablePipelineElementUnion,
PipelineElementConfig,
PipelineElementUnion,
} from '../../model/editor.model';
import { ObjectProvider } from '../../services/object-provider.service';
import {
+ AssetSaveService,
DialogService,
KeyboardShortcutService,
+ ObjectManageDialogComponent,
+ ObjectManageDialogResourceConfig,
+ ObjectManageDialogResult,
PanelType,
ShortcutRegistration,
SpBasicViewComponent,
} from '@streampipes/shared-ui';
-import { SavePipelineComponent } from
'../../dialog/save-pipeline/save-pipeline.component';
import { EditorService } from '../../services/editor.service';
import {
+ LinkageData,
Pipeline,
PipelineCanvasMetadata,
+ PermissionsService,
} from '@streampipes/platform-services';
import { JsplumbFactoryService } from '../../services/jsplumb-factory.service';
-import { forkJoin } from 'rxjs';
+import { firstValueFrom, forkJoin } from 'rxjs';
import { Router } from '@angular/router';
import { PipelineAssemblyDrawingAreaComponent } from
'./pipeline-assembly-drawing-area/pipeline-assembly-drawing-area.component';
-import { PipelineAssemblyOptionsComponent } from
'./pipeline-assembly-options/pipeline-assembly-options.component';
+import {
+ PipelineAssemblyOptionsComponent,
+ PipelineAssemblySaveOptions,
+} from './pipeline-assembly-options/pipeline-assembly-options.component';
import { JsplumbService } from '../../services/jsplumb.service';
import { TranslateService } from '@ngx-translate/core';
import { FlexDirective } from '@ngbracket/ngx-layout/flex';
+import { PipelineOperationsService } from
'../../../pipelines/services/pipeline-operations.service';
+import { IdGeneratorService } from
'../../../core-services/id-generator/id-generator.service';
+import {
+ SavePipelineComponent,
+ SavePipelineDialogResult,
+} from '../../dialog/save-pipeline/save-pipeline.component';
@Component({
selector: 'sp-pipeline-assembly',
@@ -76,6 +92,10 @@ export class PipelineAssemblyComponent implements
AfterViewInit, OnDestroy {
private jsplumbService = inject(JsplumbService);
private translateService = inject(TranslateService);
private shortcutService = inject(KeyboardShortcutService);
+ private permissionsService = inject(PermissionsService);
+ private assetSaveService = inject(AssetSaveService);
+ private pipelineOperationsService = inject(PipelineOperationsService);
+ private idGeneratorService = inject(IdGeneratorService);
@Input()
rawPipelineModel: PipelineElementConfig[];
@@ -83,6 +103,9 @@ export class PipelineAssemblyComponent implements
AfterViewInit, OnDestroy {
@Input()
originalPipeline: Pipeline;
+ @Input()
+ cloneMode = false;
+
@Input()
pipelineCanvasMetadata: PipelineCanvasMetadata;
@@ -94,6 +117,7 @@ export class PipelineAssemblyComponent implements
AfterViewInit, OnDestroy {
previewModeActive = false;
readonly: boolean;
+ private pendingManagePipelineResult?: ObjectManageDialogResult<Pipeline>;
jsplumbBridge: JsplumbBridge;
private shortcutReg: ShortcutRegistration;
@@ -146,7 +170,39 @@ export class PipelineAssemblyComponent implements
AfterViewInit, OnDestroy {
/**
* Sends the pipeline to the server
*/
- submit() {
+ submit(
+ saveOptions: PipelineAssemblySaveOptions = {
+ startPipelineAfterStorage: true,
+ },
+ ) {
+ const pipeline = this.makePipelineForSave();
+
+ if (this.originalPipeline && this.cloneMode) {
+ this.prepareClonedPipeline(pipeline);
+ this.openCreatePipelineDialog(
+ pipeline,
+ saveOptions.startPipelineAfterStorage,
+ this.makeClonedPipelineCanvasMetadata(),
+ );
+ return;
+ }
+
+ if (this.originalPipeline) {
+ void this.savePipelineChanges(
+ pipeline,
+ saveOptions.startPipelineAfterStorage,
+ );
+ return;
+ }
+
+ this.openCreatePipelineDialog(
+ pipeline,
+ saveOptions.startPipelineAfterStorage,
+ this.pipelineCanvasMetadata,
+ );
+ }
+
+ private makePipelineForSave(): Pipeline {
const pipelineModel = this.rawPipelineModel;
const pipeline = this.objectProvider.makePipeline(pipelineModel);
this.pipelinePositioningService.collectPipelineElementPositions(
@@ -157,27 +213,243 @@ export class PipelineAssemblyComponent implements
AfterViewInit, OnDestroy {
pipelineModel,
this.readonly,
);
- const dialogRef = this.dialogService.open(SavePipelineComponent, {
+ return pipeline;
+ }
+
+ private openCreatePipelineDialog(
+ pipeline: Pipeline,
+ startPipelineAfterStorage: boolean,
+ pipelineCanvasMetadata: PipelineCanvasMetadata,
+ ): void {
+ const resourceConfig: ObjectManageDialogResourceConfig<Pipeline> = {
+ resourceLabel: 'Pipeline',
+ nameLabel: 'Pipeline name',
+ descriptionLabel: 'Description',
+ nameProperty: 'name',
+ assetLinkType: 'pipeline',
+ assetLinkCheckboxLabel:
+ 'Add the current pipeline to an existing asset',
+ saveResource: async resource => {
+ const saveSuccessful = await this.savePipelineResource(
+ resource,
+ startPipelineAfterStorage,
+ false,
+ pipelineCanvasMetadata,
+ );
+ if (!saveSuccessful) {
+ throw new Error('Saving the pipeline failed.');
+ }
+ },
+ };
+ const dialogRef = this.dialogService.open(ObjectManageDialogComponent,
{
panelType: PanelType.SLIDE_IN_PANEL,
title: this.translateService.instant('Save pipeline'),
- width: '40vw',
+ width: '50vw',
data: {
- pipeline: pipeline,
- originalPipeline: this.originalPipeline,
- pipelineCanvasMetadata: this.pipelineCanvasMetadata,
+ createMode: true,
+ resource: JSON.parse(JSON.stringify(pipeline)),
+ saveMode: 'immediate',
+ resourceConfig,
+ headerTitle: this.translateService.instant('Save pipeline'),
},
});
- dialogRef
- .afterClosed()
- .subscribe((config: { reload: boolean; pipelineId: string }) => {
- if (config?.reload) {
- this.clearAssembly();
- this.rawPipelineModel = [];
- setTimeout(() => {
- this.router.navigate(['pipelines', 'create']);
- });
- }
- });
+ dialogRef.afterClosed().subscribe(refresh => {
+ if (refresh) {
+ this.editorService.makePipelineAssemblyEmpty(true);
+ this.editorService.removePipelineFromCache().subscribe();
+ this.router.navigate(['pipelines']);
+ }
+ });
+ }
+
+ managePipeline(): void {
+ if (!this.originalPipeline) {
+ return;
+ }
+
+ const resource: Pipeline = { ...this.originalPipeline };
+ const resourceConfig: ObjectManageDialogResourceConfig<Pipeline> = {
+ resourceLabel: 'Pipeline',
+ nameLabel: 'Pipeline name',
+ descriptionLabel: 'Description',
+ nameProperty: 'name',
+ assetLinkType: 'pipeline',
+ assetLinkCheckboxLabel:
+ 'Add the current pipeline to an existing asset',
+ };
+
+ const dialogRef = this.dialogService.open(ObjectManageDialogComponent,
{
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('Manage'),
+ width: '50vw',
+ data: {
+ objectInstanceId: resource._id,
+ resource,
+ saveMode: 'deferred',
+ resourceConfig,
+ headerTitle:
+ this.translateService.instant('Manage Pipeline ') +
+ resource.name,
+ },
+ });
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (result && typeof result !== 'boolean') {
+ this.pendingManagePipelineResult = result;
+ Object.assign(this.originalPipeline, result.resource);
+ }
+ });
+ }
+
+ deletePipeline(): void {
+ if (!this.originalPipeline) {
+ return;
+ }
+
+ this.pipelineOperationsService.showDeleteDialog(
+ this.originalPipeline._id,
+ this.originalPipeline.name,
+ this.originalPipeline.running,
+ new EventEmitter<boolean>(),
+ () => this.router.navigate(['pipelines']),
+ );
+ }
+
+ private async savePipelineChanges(
+ pipeline: Pipeline,
+ startPipelineAfterStorage: boolean,
+ ): Promise<void> {
+ pipeline._id = this.originalPipeline._id;
+ pipeline.name = this.originalPipeline.name;
+ pipeline.description = this.originalPipeline.description;
+ pipeline.running = this.originalPipeline.running;
+ pipeline.createdAt = this.originalPipeline.createdAt;
+ pipeline.createdByUser = this.originalPipeline.createdByUser;
+
+ const saveSuccessful = await this.savePipelineResource(
+ pipeline,
+ startPipelineAfterStorage,
+ true,
+ this.pipelineCanvasMetadata,
+ );
+ if (!saveSuccessful) {
+ return;
+ }
+
+ await this.savePendingManagePipelineChanges();
+ this.editorService.makePipelineAssemblyEmpty(true);
+ this.editorService.removePipelineFromCache().subscribe();
+ this.router.navigate(['pipelines']);
+ }
+
+ private async savePipelineResource(
+ pipeline: Pipeline,
+ startPipelineAfterStorage: boolean,
+ updateExisting: boolean,
+ pipelineCanvasMetadata: PipelineCanvasMetadata,
+ ): Promise<boolean> {
+ const dialogRef = this.dialogService.open(SavePipelineComponent, {
+ panelType: PanelType.STANDARD_PANEL,
+ title: this.translateService.instant('Save pipeline'),
+ width: '70vw',
+ disableClose: true,
+ data: {
+ pipeline,
+ originalPipeline: updateExisting
+ ? this.originalPipeline
+ : undefined,
+ pipelineCanvasMetadata,
+ startPipelineAfterStorage,
+ updateExisting,
+ },
+ });
+
+ const result = (await firstValueFrom(
+ dialogRef.afterClosed(),
+ )) as SavePipelineDialogResult;
+ if (result?.pipelineId) {
+ pipeline._id = result.pipelineId;
+ }
+ return !!result?.success;
+ }
+
+ private async savePendingManagePipelineChanges(): Promise<void> {
+ const result = this.pendingManagePipelineResult;
+ if (!result) {
+ return;
+ }
+
+ if (result.permission) {
+ await firstValueFrom(
+ this.permissionsService.updatePermission(result.permission),
+ );
+ }
+
+ if (this.shouldSaveManagePipelineAssets(result)) {
+ await this.assetSaveService.saveSelectedAssets(
+ result.selectedAssets,
+ this.createPipelineLinkageData(result.resource),
+ result.deselectedAssets,
+ result.originalAssets,
+ );
+ }
+
+ this.pendingManagePipelineResult = undefined;
+ }
+
+ private shouldSaveManagePipelineAssets(
+ result: ObjectManageDialogResult<Pipeline>,
+ ): boolean {
+ return (
+ result.addToAssets &&
+ (result.selectedAssets.length > 0 ||
+ result.deselectedAssets.length > 0 ||
+ result.originalAssets.length > 0)
+ );
+ }
+
+ private createPipelineLinkageData(pipeline: Pipeline): LinkageData[] {
+ return [
+ {
+ type: 'pipeline',
+ id: pipeline._id ?? '',
+ name: pipeline.name ?? '',
+ },
+ ];
+ }
+
+ private prepareClonedPipeline(pipeline: Pipeline): void {
+ pipeline._id = undefined;
+ pipeline._rev = undefined;
+ pipeline.name = `${this.originalPipeline.name}_cloned`;
+ pipeline.description = this.originalPipeline.description;
+ pipeline.running = false;
+ pipeline.actions.forEach(element =>
+ this.updateInvocablePipelineElementId(element),
+ );
+ pipeline.sepas.forEach(element =>
+ this.updateInvocablePipelineElementId(element),
+ );
+ }
+
+ private updateInvocablePipelineElementId(
+ entity: InvocablePipelineElementUnion,
+ ): void {
+ const lastIdIndex = entity.elementId.lastIndexOf(':');
+ entity.elementId =
+ entity.elementId.substring(0, lastIdIndex + 1) +
+ this.idGeneratorService.generate(5);
+ }
+
+ private makeClonedPipelineCanvasMetadata(): PipelineCanvasMetadata {
+ const metadata = PipelineCanvasMetadata.fromData(
+ this.pipelineCanvasMetadata,
+ new PipelineCanvasMetadata(),
+ );
+ metadata._id = undefined;
+ metadata._rev = undefined;
+ metadata.pipelineId = undefined;
+ return metadata;
}
togglePreview(): void {
diff --git
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
deleted file mode 100644
index 2fa9e2902a..0000000000
---
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.html
+++ /dev/null
@@ -1,177 +0,0 @@
-<!--
-~ Licensed to the Apache Software Foundation (ASF) under one or more
-~ contributor license agreements. See the NOTICE file distributed with
-~ this work for additional information regarding copyright ownership.
-~ The ASF licenses this file to You under the Apache License, Version 2.0
-~ (the "License"); you may not use this file except in compliance with
-~ the License. You may obtain a copy of the License at
-~
-~ http://www.apache.org/licenses/LICENSE-2.0
-~
-~ Unless required by applicable law or agreed to in writing, software
-~ distributed under the License is distributed on an "AS IS" BASIS,
-~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-~ See the License for the specific language governing permissions and
-~ limitations under the License.
-~
--->
-
-<div fxLayout="column">
- <form [formGroup]="submitPipelineForm">
- <div fxFlex="100" fxLayout="column">
- <sp-form-field [level]="3" [label]="'Pipeline Name' | translate">
- <mat-form-field fxFlex color="accent">
- <input
- [formControlName]="'pipelineName'"
- data-cy="sp-editor-pipeline-name"
- matInput
- name="pipelineName"
- (blur)="triggerTutorial()"
- />
- @if (
- submitPipelineForm
- .get('pipelineName')
- .hasError('required') ||
- submitPipelineForm
- .get('pipelineName')
- .hasError('whiteSpaceOnly')
- ) {
- <mat-error>
- {{ 'Pipeline name is required.' | translate }}
- </mat-error>
- }
- @if (
- submitPipelineForm
- .get('pipelineName')
- .hasError('leadingOrTrailingWhitespace')
- ) {
- <mat-error>
- {{
- 'Pipeline name cannot start or end with a
space.'
- | translate
- }}
- </mat-error>
- }
- @if (
- submitPipelineForm
- .get('pipelineName')
- .hasError('minlength') ||
- submitPipelineForm
- .get('pipelineName')
- .hasError('maxlength')
- ) {
- <mat-error>
- {{
- 'Pipeline name must have between 3 and 50
characters.'
- | translate
- }}
- </mat-error>
- }
- @if (
- submitPipelineForm
- .get('pipelineName')
- .hasError('invalidName')
- ) {
- <mat-error>
- {{
- 'Pipeline name can only contain letters,
numbers, dashes (-), and underscores (_).'
- | translate
- }}
- </mat-error>
- }
- </mat-form-field>
- </sp-form-field>
- <sp-form-field [level]="3" [label]="'Description' | translate">
- <mat-form-field fxFlex color="accent">
- <input [formControlName]="'pipelineDescription'" matInput
/>
- <mat-error
- >{{
- 'Pipeline description must not have more than 80
characters.'
- | translate
- }}
- </mat-error>
- </mat-form-field>
- </sp-form-field>
- </div>
- </form>
- @if (storageOptions.updateModeActive) {
- <div id="overwriteCheckbox" class="checkbox">
- <mat-radio-group
- [(ngModel)]="storageOptions.updateMode"
- fxLayout="column"
- color="accent"
- class="pipeline-radio-group"
- >
- <mat-radio-button
- [value]="'update'"
- style="padding-left: 0"
- data-cy="pipeline-update-mode-update"
- >
- {{ 'Update pipeline' | translate }}
- </mat-radio-button>
- <mat-radio-button
- [value]="'clone'"
- class="mb-10"
- data-cy="pipeline-update-mode-clone"
- >
- {{ 'Create new pipeline' | translate }}
- </mat-radio-button>
- </mat-radio-group>
- </div>
- }
- <sp-split-section [level]="3" [title]="'Options' | translate">
- <mat-checkbox
- [(ngModel)]="storageOptions.startPipelineAfterStorage"
- color="accent"
- data-cy="sp-editor-checkbox-start-immediately"
- >
- {{ 'Start pipeline immediately' | translate }}
- </mat-checkbox>
- <mat-checkbox
- [(ngModel)]="storageOptions.navigateToPipelineOverview"
- color="accent"
- data-cy="sp-editor-checkbox-navigate-to-overview"
- >
- {{ 'Navigate to pipeline overview afterwards' | translate }}
- </mat-checkbox>
- @if (isAssetAdmin) {
- <mat-checkbox
- [(ngModel)]="addToAssets"
- color="accent"
- data-cy="sp-show-pipeline-asset-checkbox"
- >
- {{ 'Add Pipeline to Assets' | translate }}
- </mat-checkbox>
- @if (addToAssets) {
- <div class="mt-10">
- <sp-asset-link-configuration
- [isEdit]="storageOptions.updateMode === 'update'"
- [itemId]="pipeline._id"
- (selectedAssetsChange)="onSelectedAssetsChange($event)"
- (deselectedAssetsChange)="
- onDeselectedAssetsChange($event)
- "
- (originalAssetsEmitter)="
- onOriginalAssetsEmitted($event)
- "
- >
- </sp-asset-link-configuration>
- </div>
- }
- }
- </sp-split-section>
- <div class="mt-10">
- <mat-expansion-panel class="mat-elevation-z0 border-1">
- <mat-expansion-panel-header>{{
- 'Show pipeline configuration as code' | translate
- }}</mat-expansion-panel-header>
- @if (compactPipeline) {
- <sp-configuration-code-panel
- [configuration]="compactPipeline"
- maxHeight="none"
- >
- </sp-configuration-code-panel>
- }
- </mat-expansion-panel>
- </div>
-</div>
diff --git
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
deleted file mode 100644
index e22ed79985..0000000000
---
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-.border-1 {
- border: 1px solid var(--color-bg-2);
-}
diff --git
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
deleted file mode 100644
index 2b82cb626c..0000000000
---
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline-settings/save-pipeline-settings.component.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import {
- Component,
- EventEmitter,
- inject,
- Input,
- OnInit,
- Output,
-} from '@angular/core';
-import { ShepherdService } from '../../../../services/tour/shepherd.service';
-import {
- FormsModule,
- ReactiveFormsModule,
- UntypedFormControl,
- UntypedFormGroup,
- Validators,
-} from '@angular/forms';
-import {
- CompactPipeline,
- Pipeline,
- PipelineService,
- SpAssetTreeNode,
- UserInfo,
-} from '@streampipes/platform-services';
-import { PipelineStorageOptions } from '../../../model/editor.model';
-import { ValidateName } from
'../../../../core-ui/static-properties/input.validator';
-import {
- AssetLinkConfigurationComponent,
- CurrentUserService,
- FormFieldComponent,
- SplitSectionComponent,
-} from '@streampipes/shared-ui';
-import { UserRole } from '../../../../core/auth/user-role.enum';
-import { FlexDirective, LayoutDirective } from '@ngbracket/ngx-layout/flex';
-import { MatError, MatFormField } from '@angular/material/form-field';
-import { MatInput } from '@angular/material/input';
-import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
-import { MatCheckbox } from '@angular/material/checkbox';
-import {
- MatExpansionPanel,
- MatExpansionPanelHeader,
-} from '@angular/material/expansion';
-import { ConfigurationCodePanelComponent } from
'../../../../core-ui/configuration-code-panel/configuration-code-panel.component';
-import { TranslatePipe } from '@ngx-translate/core';
-
-@Component({
- selector: 'sp-save-pipeline-settings',
- templateUrl: './save-pipeline-settings.component.html',
- styleUrls: ['./save-pipeline-settings.component.scss'],
- imports: [
- LayoutDirective,
- FormsModule,
- ReactiveFormsModule,
- FlexDirective,
- FormFieldComponent,
- MatFormField,
- MatInput,
- MatError,
- MatRadioGroup,
- MatRadioButton,
- SplitSectionComponent,
- MatCheckbox,
- AssetLinkConfigurationComponent,
- MatExpansionPanel,
- MatExpansionPanelHeader,
- ConfigurationCodePanelComponent,
- TranslatePipe,
- ],
-})
-export class SavePipelineSettingsComponent implements OnInit {
- private readonly currentUserService = inject(CurrentUserService);
-
- @Input()
- submitPipelineForm: UntypedFormGroup = new UntypedFormGroup({});
-
- @Input()
- pipeline: Pipeline;
-
- @Input()
- storageOptions: PipelineStorageOptions;
-
- @Input()
- currentPipelineName: string;
-
- private shepherdService = inject(ShepherdService);
- private pipelineService = inject(PipelineService);
-
- compactPipeline: CompactPipeline;
- currentUser: UserInfo;
- isAssetAdmin = false;
-
- addToAssets: boolean = false;
- @Input()
- selectedAssets: SpAssetTreeNode[];
- @Input()
- deselectedAssets: SpAssetTreeNode[];
- @Input()
- originalAssets: SpAssetTreeNode[];
-
- @Output() selectedAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
- @Output() deselectedAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
- @Output() originalAssetsChange = new EventEmitter<SpAssetTreeNode[]>();
-
- ngOnInit() {
- this.currentUser = this.currentUserService.getCurrentUser();
- this.isAssetAdmin = this.currentUserService.hasRole(
- UserRole.ROLE_ASSET_ADMIN,
- );
- this.submitPipelineForm.addControl(
- 'pipelineName',
- new UntypedFormControl(this.pipeline.name, [
- Validators.required,
- Validators.minLength(3),
- Validators.maxLength(50),
- ValidateName(),
- ]),
- );
- this.submitPipelineForm.addControl(
- 'pipelineDescription',
- new UntypedFormControl(this.pipeline.description, [
- Validators.maxLength(80),
- ]),
- );
-
-
this.submitPipelineForm.controls['pipelineName'].valueChanges.subscribe(
- value => {
- this.pipeline.name = value;
- },
- );
-
- this.submitPipelineForm.controls[
- 'pipelineDescription'
- ].valueChanges.subscribe(value => {
- this.pipeline.description = value;
- });
- this.pipelineService
- .convertToCompactPipeline(this.pipeline)
- .subscribe(p => (this.compactPipeline = p));
- if (this.storageOptions.updateModeActive) {
- this.addToAssets = true;
- }
- }
-
- onSelectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
- this.selectedAssets = updatedAssets;
- this.selectedAssetsChange.emit(this.selectedAssets);
- }
-
- onDeselectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
- this.deselectedAssets = updatedAssets;
- this.deselectedAssetsChange.emit(this.deselectedAssets);
- }
-
- onOriginalAssetsEmitted(updatedAssets: SpAssetTreeNode[]): void {
- this.originalAssets = updatedAssets;
- this.originalAssetsChange.emit(this.originalAssets);
- }
-
- triggerTutorial() {
- if (this.shepherdService.isTourActive()) {
- this.shepherdService.trigger('save-pipeline-dialog');
- }
- }
-}
diff --git
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
index 51004cc737..dfd13185d1 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.html
@@ -17,98 +17,64 @@
-->
<div class="sp-dialog-container">
- <div class="sp-dialog-content padding-20">
- <div fxFlex="100" fxLayout="column">
- @if (
- !operationCompleted &&
- !operationProgress &&
- !pipelineUpdatePreflight
- ) {
- <sp-save-pipeline-settings
- [currentPipelineName]="pipeline.name"
- [submitPipelineForm]="submitPipelineForm"
- [pipeline]="pipeline"
- [storageOptions]="storageOptions"
- [(selectedAssets)]="selectedAssets"
- [(deselectedAssets)]="deselectedAssets"
- [(originalAssets)]="originalAssets"
- >
- </sp-save-pipeline-settings>
- }
-
- @if (
- pipelineUpdatePreflight &&
- !operationCompleted &&
- !operationProgress
- ) {
- <sp-save-pipeline-update-migration
- [measurementUpdateInfos]="measurementUpdateInfos"
- (startUpdateEmitter)="savePipeline(true)"
- >
- </sp-save-pipeline-update-migration>
- }
-
- @if (operationProgress || operationCompleted) {
- <sp-multi-step-status-indicator
- [statusIndicators]="statusIndicators"
- >
- </sp-multi-step-status-indicator>
- }
+ <div class="sp-dialog-content p-15">
+ <div fxLayout="column" fxLayoutAlign="center center" fxFlex="100">
+ <div fxFlex="100" fxLayout="column" class="w-100">
+ @if (
+ pipelineUpdatePreflight &&
+ !operationCompleted &&
+ !operationProgress
+ ) {
+ <sp-save-pipeline-update-migration
+ fxFlex="100"
+ [measurementUpdateInfos]="measurementUpdateInfos"
+ (startUpdateEmitter)="savePipeline(true)"
+ >
+ </sp-save-pipeline-update-migration>
+ }
- @if (finalPipelineOperationStatus) {
- <div class="mt-10">
- <mat-divider></mat-divider>
- <sp-pipeline-started-status
- class="mt-10"
- [forceStopDisabled]="true"
- [action]="pipelineAction"
-
[pipelineOperationStatus]="finalPipelineOperationStatus"
+ @if (operationProgress || operationCompleted) {
+ <sp-multi-step-status-indicator
+ [statusIndicators]="statusIndicators"
>
- </sp-pipeline-started-status>
- </div>
- }
+ </sp-multi-step-status-indicator>
+ }
+
+ @if (finalPipelineOperationStatus) {
+ <div class="mt-10">
+ <mat-divider></mat-divider>
+ <sp-pipeline-started-status
+ class="mt-10"
+ [forceStopDisabled]="true"
+ [action]="pipelineAction"
+ [pipelineOperationStatus]="
+ finalPipelineOperationStatus
+ "
+ >
+ </sp-pipeline-started-status>
+ </div>
+ }
+ </div>
</div>
</div>
<mat-divider></mat-divider>
- <div class="sp-dialog-actions" fxLayoutGap="10px">
- @if (operationCompleted) {
- <button mat-flat-button (click)="hide(false)">
- Create another pipeline
- </button>
- }
+ <div class="sp-dialog-actions actions-align-right" fxLayoutGap="10px">
@if (operationCompleted) {
<button
mat-flat-button
class="mat-basic"
- data-cy="sp-navigate-to-pipeline-overview"
- (click)="navigateToPipelineOverview()"
- >
- Open pipeline overview
- </button>
- }
- @if (
- !operationCompleted && !operationSuccess &&
!pipelineUpdatePreflight
- ) {
- <button
- [disabled]="
- !submitPipelineForm.valid ||
- operationProgress ||
- operationCompleted
- "
- mat-flat-button
- color="accent"
- (click)="savePipeline()"
- data-cy="sp-editor-apply"
+ data-cy="sp-save-pipeline-status-close"
+ (click)="close()"
>
- {{ 'Apply' | translate }}
+ {{ 'Close' | translate }}
</button>
}
- @if (!operationProgress && (!operationCompleted || !operationSuccess))
{
+ @if (pipelineUpdatePreflight && !operationProgress) {
<button
mat-flat-button
class="mat-basic"
- (click)="hide(true)"
data-cy="sp-editor-cancel"
+ (click)="close()"
>
{{ 'Cancel' | translate }}
</button>
diff --git
a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
index cdfe259c98..13cbc4aacb 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.scss
@@ -15,21 +15,3 @@
* limitations under the License.
*
*/
-
-.customize-section {
- display: flex;
- flex: 1 1 auto;
- padding: 20px;
-}
-
-.padding-20 {
- padding: 20px;
-}
-
-.mb-10 {
- margin-bottom: 10px;
-}
-
-::ng-deep .pipeline-radio-group .mat-radio-label {
- padding: 0;
-}
diff --git a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
index 1cb1fa125c..a6343f925e 100644
--- a/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
+++ b/ui/src/app/editor/dialog/save-pipeline/save-pipeline.component.ts
@@ -16,32 +16,18 @@
*
*/
-import { Component, inject, Input, OnInit } from '@angular/core';
-import { AssetSaveService, DialogRef } from '@streampipes/shared-ui';
+import { Component, Input, OnInit, inject } from '@angular/core';
+import { DialogRef } from '@streampipes/shared-ui';
import {
- DatalakeRestService,
- DataSinkInvocation,
- LinkageData,
+ MeasurementUpdateInfo,
Message,
Pipeline,
PipelineCanvasMetadata,
PipelineCanvasMetadataService,
PipelineOperationStatus,
- MeasurementUpdateInfo,
PipelineService,
- SpAssetTreeNode,
} from '@streampipes/platform-services';
-import { EditorService } from '../../services/editor.service';
-import { ShepherdService } from '../../../services/tour/shepherd.service';
-import { UntypedFormGroup } from '@angular/forms';
-import { Router } from '@angular/router';
-import {
- InvocablePipelineElementUnion,
- PipelineStorageOptions,
-} from '../../model/editor.model';
-import { IdGeneratorService } from
'../../../core-services/id-generator/id-generator.service';
-import { firstValueFrom, lastValueFrom, Observable, of, tap } from 'rxjs';
-import { filter, switchMap } from 'rxjs/operators';
+import { firstValueFrom } from 'rxjs';
import {
Status,
StatusIndicator,
@@ -53,13 +39,17 @@ import {
LayoutDirective,
LayoutGapDirective,
} from '@ngbracket/ngx-layout/flex';
-import { SavePipelineSettingsComponent } from
'./save-pipeline-settings/save-pipeline-settings.component';
import { MultiStepStatusIndicatorComponent } from
'../../../core-ui/multi-step-status-indicator/multi-step-status-indicator.component';
import { MatDivider } from '@angular/material/divider';
import { PipelineStartedStatusComponent } from
'../../../core-ui/pipeline/pipeline-started-status/pipeline-started-status.component';
import { MatButton } from '@angular/material/button';
import { SavePipelineUpdateMigrationComponent } from
'./save-pipeline-update-migration/save-pipeline-update-migration.component';
+export interface SavePipelineDialogResult {
+ success: boolean;
+ pipelineId?: string;
+}
+
@Component({
selector: 'sp-save-pipeline',
templateUrl: './save-pipeline.component.html',
@@ -67,178 +57,98 @@ import { SavePipelineUpdateMigrationComponent } from
'./save-pipeline-update-mig
imports: [
FlexDirective,
LayoutDirective,
- SavePipelineSettingsComponent,
+ LayoutGapDirective,
MultiStepStatusIndicatorComponent,
MatDivider,
PipelineStartedStatusComponent,
- LayoutGapDirective,
MatButton,
TranslatePipe,
SavePipelineUpdateMigrationComponent,
],
})
export class SavePipelineComponent implements OnInit {
- private editorService = inject(EditorService);
private dialogRef = inject(DialogRef<SavePipelineComponent>);
- private idGeneratorService = inject(IdGeneratorService);
private pipelineService = inject(PipelineService);
- private router = inject(Router);
- private shepherdService = inject(ShepherdService);
private pipelineCanvasService = inject(PipelineCanvasMetadataService);
- private assetSaveService = inject(AssetSaveService);
- private dataLakeService = inject(DatalakeRestService);
private translateService = inject(TranslateService);
@Input()
pipeline: Pipeline;
@Input()
- originalPipeline: Pipeline;
-
- selectedAssets: SpAssetTreeNode[];
- deselectedAssets: SpAssetTreeNode[];
- originalAssets: SpAssetTreeNode[];
+ originalPipeline?: Pipeline;
@Input()
pipelineCanvasMetadata: PipelineCanvasMetadata;
+ @Input()
+ startPipelineAfterStorage = true;
+
+ @Input()
+ updateExisting = false;
+
operationProgress = false;
operationCompleted = false;
operationSuccess = false;
- errorMessage = '';
pipelineId: string;
-
- storageOptions: PipelineStorageOptions = {
- updateMode: 'update',
- startPipelineAfterStorage: true,
- navigateToPipelineOverview: true,
- updateModeActive: false,
- };
-
- submitPipelineForm: UntypedFormGroup = new UntypedFormGroup({});
statusIndicators: StatusIndicator[] = [];
- finalPipelineOperationStatus: PipelineOperationStatus;
- pipelineAction: PipelineAction;
+ finalPipelineOperationStatus?: PipelineOperationStatus;
+ pipelineAction?: PipelineAction;
pipelineUpdatePreflight = false;
measurementUpdateInfos: MeasurementUpdateInfo[] = [];
- ngOnInit() {
- this.storageOptions.updateModeActive =
- this.originalPipeline !== undefined;
- if (this.storageOptions.updateModeActive) {
- this.pipeline._id = this.originalPipeline._id;
- this.pipeline.name = this.originalPipeline.name;
- this.pipeline.description = this.originalPipeline.description;
- this.pipeline.running = this.originalPipeline.running;
- this.pipeline.createdAt = this.originalPipeline.createdAt;
- this.pipeline.createdByUser = this.originalPipeline.createdByUser;
- }
-
- if (this.shepherdService.isTourActive()) {
- this.shepherdService.trigger('enter-pipeline-name');
- }
+ ngOnInit(): void {
+ void this.savePipeline();
}
- performStorageOperations(
- stopPipeline$: Observable<null | PipelineOperationStatus>,
- savePipeline$: Observable<Message>,
- ) {
- // if pipeline is running and update mode: stop pipeline
- // if update mode: update pipeline, if not update mode or update mode
clone: save pipeline
- // if update mode and not clone: update canvas, else store new canvas
- // if should start: start pipeline
- stopPipeline$
- .pipe(
- tap(() =>
- this.addStatusIndicator(
- this.translateService.instant('Saving pipeline'),
- Status.PROGRESS,
- ),
- ),
- switchMap(() => savePipeline$),
- tap(message => {
- this.operationSuccess = message.success;
- if (!message.success) {
- this.handleStorageError();
- }
- this.modifyStatusIndicator(Status.SUCCESS);
- this.pipelineId = message.notifications[1].description;
- }),
- // only continue if pipeline was saved
- filter(message => message.success),
- tap(() =>
- this.addStatusIndicator(
- this.translateService.instant('Saving metadata'),
- Status.PROGRESS,
- ),
- ),
- switchMap(() =>
- this.getPipelineCanvasMetadata$(this.pipelineId),
- ),
- tap(() => this.modifyStatusIndicator(Status.SUCCESS)),
- switchMap(() => this.getStartPipeline$()),
- )
- .subscribe({
- next: message => {
- this.onSuccess(message);
- // Add Asset as soon as pipelineId is known
- this.addToAsset();
- },
- error: msg => {
- this.onFailure(msg);
- },
- });
- }
-
- clonePipeline(): void {
- this.pipeline._id = undefined;
- this.pipeline._rev = undefined;
- this.pipeline.running = false;
- this.pipeline.actions.forEach(element => this.updateId(element));
- this.pipeline.sepas.forEach(element => this.updateId(element));
- this.pipelineCanvasMetadata._id = undefined;
- this.pipelineCanvasMetadata._rev = undefined;
- }
-
- savePipeline(skipPreflight = false) {
- if (this.shouldPerformUpdatePreflight(skipPreflight)) {
- this.performUpdatePreflight();
+ async savePipeline(skipPreflight = false): Promise<void> {
+ if (await this.shouldPerformUpdatePreflight(skipPreflight)) {
+ await this.performUpdatePreflight();
return;
}
this.pipelineUpdatePreflight = false;
- let stopPipeline$: Observable<null | PipelineOperationStatus> =
- of(null);
- let savePipeline$: Observable<Message> =
- this.pipelineService.storePipeline(this.pipeline);
this.operationProgress = true;
- if (this.storageOptions.updateModeActive) {
- if (this.storageOptions.updateMode === 'clone') {
- this.clonePipeline();
- } else {
- if (this.pipeline.running) {
- stopPipeline$ = this.getStopPipeline$();
+
+ try {
+ if (this.updateExisting && this.pipeline.running) {
+ const stopResult = await this.stopPipeline();
+ if (!stopResult.success) {
+ return;
}
- savePipeline$ = this.pipelineService.updatePipeline(
- this.pipeline,
- );
}
- }
- this.performStorageOperations(stopPipeline$, savePipeline$);
+ const saveMessage = await this.storeOrUpdatePipeline();
+ if (!saveMessage.success) {
+ this.handleStorageError();
+ return;
+ }
+
+ this.pipelineId = this.getPipelineId(saveMessage);
+ if (!this.pipelineId) {
+ this.handleStorageError();
+ return;
+ }
+
+ this.pipeline._id = this.pipelineId;
+ await this.savePipelineCanvasMetadata();
+ if (!(await this.startPipelineIfRequested())) {
+ return;
+ }
+ this.onSuccess();
+ } catch {
+ this.onFailure();
+ }
}
- shouldPerformUpdatePreflight(skipPreflight: boolean): boolean {
- return (
- !skipPreflight &&
- this.storageOptions.updateModeActive &&
- this.storageOptions.updateMode !== 'clone' &&
- this.hasDataLakeSink()
- );
+ private async shouldPerformUpdatePreflight(
+ skipPreflight: boolean,
+ ): Promise<boolean> {
+ return !skipPreflight && this.updateExisting && this.hasDataLakeSink();
}
- hasDataLakeSink(): boolean {
+ private hasDataLakeSink(): boolean {
return this.pipeline.actions.some(
action =>
action.appId ===
@@ -246,122 +156,131 @@ export class SavePipelineComponent implements OnInit {
);
}
- performUpdatePreflight(): void {
+ private async performUpdatePreflight(): Promise<void> {
this.operationProgress = true;
this.addStatusIndicator(
this.translateService.instant('Checking pipeline update'),
Status.PROGRESS,
);
- this.pipelineService
- .performPipelineMigrationPreflight(this.pipeline)
- .subscribe({
- next: updateInfos => {
- if (updateInfos.length === 0) {
- this.modifyStatusIndicator(Status.SUCCESS);
- this.savePipeline(true);
- } else {
- this.measurementUpdateInfos = updateInfos;
- this.pipelineUpdatePreflight = true;
- this.operationProgress = false;
- this.statusIndicators = [];
- }
- },
- error: msg => {
- this.onFailure(msg);
- },
- });
- }
-
- updateId(entity: InvocablePipelineElementUnion) {
- const lastIdIndex = entity.elementId.lastIndexOf(':');
- entity.elementId =
- entity.elementId.substring(0, lastIdIndex + 1) +
- this.idGeneratorService.generate(5);
- }
- getStopPipeline$(): Observable<PipelineOperationStatus> {
- return of(null).pipe(
- tap(() =>
- this.addStatusIndicator(
- this.translateService.instant('Stopping pipeline'),
- Status.PROGRESS,
+ try {
+ const updateInfos = await firstValueFrom(
+ this.pipelineService.performPipelineMigrationPreflight(
+ this.pipeline,
),
- ),
- switchMap(() =>
- this.pipelineService.stopPipeline(this.originalPipeline._id),
- ),
- tap(msg => {
- this.operationSuccess = msg.success;
- if (!msg.success) {
- this.handlePipelineOperationError(msg,
PipelineAction.Stop);
- } else {
- this.modifyStatusIndicator(Status.SUCCESS);
- }
- }),
- filter(status => status.success),
- );
+ );
+
+ if (updateInfos.length === 0) {
+ this.modifyStatusIndicator(Status.SUCCESS);
+ await this.savePipeline(true);
+ } else {
+ this.measurementUpdateInfos = updateInfos;
+ this.pipelineUpdatePreflight = true;
+ this.operationProgress = false;
+ this.statusIndicators = [];
+ }
+ } catch {
+ this.onFailure();
+ }
}
- getStartPipeline$(): Observable<null | PipelineOperationStatus> {
- if (this.storageOptions.startPipelineAfterStorage) {
- return of(null).pipe(
- tap(() =>
- this.addStatusIndicator(
- this.translateService.instant('Starting pipeline'),
- Status.PROGRESS,
- ),
- ),
- switchMap(() =>
- this.pipelineService.startPipeline(this.pipelineId),
- ),
- tap(msg => {
- if (!msg.success) {
- this.handlePipelineOperationError(
- msg,
- PipelineAction.Start,
- );
- } else {
- this.modifyStatusIndicator(
- msg.success ? Status.SUCCESS : Status.FAILURE,
- );
- }
- }),
- );
+ private async stopPipeline(): Promise<PipelineOperationStatus> {
+ this.addStatusIndicator(
+ this.translateService.instant('Stopping pipeline'),
+ Status.PROGRESS,
+ );
+ const stopResult = await firstValueFrom(
+ this.pipelineService.stopPipeline(this.originalPipeline._id),
+ );
+ this.operationSuccess = stopResult.success;
+ if (!stopResult.success) {
+ this.handlePipelineOperationError(stopResult, PipelineAction.Stop);
} else {
- return of(null);
+ this.modifyStatusIndicator(Status.SUCCESS);
}
+ return stopResult;
}
- getPipelineCanvasMetadata$(pipelineId: string): Observable<object> {
- this.pipelineCanvasMetadata.pipelineId = pipelineId;
- return this.pipelineCanvasService.updatePipelineCanvasMetadata(
- pipelineId,
- this.pipelineCanvasMetadata,
+ private async storeOrUpdatePipeline(): Promise<Message> {
+ this.addStatusIndicator(
+ this.translateService.instant('Saving pipeline'),
+ Status.PROGRESS,
);
+ const saveMessage = this.updateExisting
+ ? await firstValueFrom(
+ this.pipelineService.updatePipeline(this.pipeline),
+ )
+ : await firstValueFrom(
+ this.pipelineService.storePipeline(this.pipeline),
+ );
+ this.operationSuccess = saveMessage.success;
+ this.modifyStatusIndicator(
+ saveMessage.success ? Status.SUCCESS : Status.FAILURE,
+ );
+ return saveMessage;
}
- addStatusIndicator(message: string, status: Status) {
- this.statusIndicators.push({ message, status });
+ private getPipelineId(saveMessage: Message): string {
+ return (
+ (this.updateExisting ? this.originalPipeline?._id : undefined) ??
+ saveMessage.notifications?.[1]?.description
+ );
}
- modifyStatusIndicator(status: Status) {
- // modify status of the last indicator
- this.statusIndicators[this.statusIndicators.length - 1].status =
status;
+ private async savePipelineCanvasMetadata(): Promise<void> {
+ this.addStatusIndicator(
+ this.translateService.instant('Saving metadata'),
+ Status.PROGRESS,
+ );
+ this.pipelineCanvasMetadata.pipelineId = this.pipelineId;
+ await firstValueFrom(
+ this.pipelineCanvasService.updatePipelineCanvasMetadata(
+ this.pipelineId,
+ this.pipelineCanvasMetadata,
+ ),
+ );
+ this.modifyStatusIndicator(Status.SUCCESS);
}
- handleStorageError(): void {
+ private async startPipelineIfRequested(): Promise<boolean> {
+ if (!this.startPipelineAfterStorage) {
+ return true;
+ }
+
+ this.addStatusIndicator(
+ this.translateService.instant('Starting pipeline'),
+ Status.PROGRESS,
+ );
+ const startResult = await firstValueFrom(
+ this.pipelineService.startPipeline(this.pipelineId),
+ );
+ if (!startResult.success) {
+ this.handlePipelineOperationError(
+ startResult,
+ PipelineAction.Start,
+ );
+ return false;
+ } else {
+ this.modifyStatusIndicator(Status.SUCCESS);
+ this.showPipelineOperationStatus(startResult,
PipelineAction.Start);
+ }
+ return true;
+ }
+
+ private handleStorageError(): void {
this.onFailure();
}
- handlePipelineOperationError(
+ private handlePipelineOperationError(
status: PipelineOperationStatus,
pipelineAction: PipelineAction,
- ) {
+ ): void {
this.onFailure();
this.showPipelineOperationStatus(status, pipelineAction);
}
- onFailure(_msg?: any) {
+ private onFailure(): void {
+ this.operationProgress = false;
this.operationCompleted = true;
this.operationSuccess = false;
if (this.statusIndicators.length > 0) {
@@ -369,97 +288,32 @@ export class SavePipelineComponent implements OnInit {
}
}
- showPipelineOperationStatus(
+ private showPipelineOperationStatus(
status: PipelineOperationStatus,
pipelineAction: PipelineAction,
- ) {
+ ): void {
this.finalPipelineOperationStatus = status;
this.pipelineAction = pipelineAction;
}
- onSuccess(status?: PipelineOperationStatus) {
+ private onSuccess(): void {
this.operationProgress = false;
this.operationCompleted = true;
- if (status) {
- this.showPipelineOperationStatus(status, PipelineAction.Start);
- }
- this.editorService.makePipelineAssemblyEmpty(true);
- this.editorService.removePipelineFromCache().subscribe();
- if (this.shepherdService.isTourActive()) {
- this.shepherdService.hideCurrentStep();
- }
- if (this.storageOptions.navigateToPipelineOverview && status?.success)
{
- this.navigateToPipelineOverview();
- }
+ this.operationSuccess = true;
}
- navigateToPipelineOverview(): void {
- this.hide(true);
- this.router.navigate(['pipelines']);
- }
-
- hide(skipReload: boolean) {
- let reloadConfig = undefined;
- if (!skipReload) {
- reloadConfig = this.operationSuccess
- ? { reload: true, pipelineId: this.pipelineId }
- : undefined;
- }
- this.dialogRef.close(reloadConfig);
+ addStatusIndicator(message: string, status: Status): void {
+ this.statusIndicators.push({ message, status });
}
- async addToAsset(): Promise<void> {
- let linkageData: LinkageData[] = [];
- linkageData = await this.addPipelineLinkageData(linkageData);
-
- await this.saveAssets(linkageData);
- }
- private async addPipelineLinkageData(
- linkageData: LinkageData[],
- ): Promise<LinkageData[]> {
- const pipeline = await firstValueFrom(
- this.pipelineService.getPipelineById(this.pipelineId),
- );
-
- linkageData.push({
- type: 'pipeline',
- id: this.pipelineId,
- name: pipeline.name,
- });
-
- const serviceList: DataSinkInvocation[] =
- pipeline.actions as DataSinkInvocation[];
- const dataSinkServices: DataSinkInvocation[] = serviceList.filter(
- action => action.serviceTagPrefix === 'DATA_SINK',
- );
-
- for (const service of dataSinkServices) {
- const staticProperty = service.staticProperties.find(
- prop => prop.internalName === 'db_measurement',
- );
-
- const measureFromPipeline = (staticProperty as { value: string })
- .value;
-
- const measure = await lastValueFrom(
- this.dataLakeService.getMeasurementByName(measureFromPipeline),
- );
-
- linkageData.push({
- type: 'measurement',
- id: measure.elementId,
- name: measureFromPipeline,
- });
- }
- return linkageData;
+ modifyStatusIndicator(status: Status): void {
+ this.statusIndicators[this.statusIndicators.length - 1].status =
status;
}
- private async saveAssets(linkageData: LinkageData[]): Promise<void> {
- await this.assetSaveService.saveSelectedAssets(
- this.selectedAssets,
- linkageData,
- this.deselectedAssets,
- this.originalAssets,
- );
+ close(): void {
+ this.dialogRef.close({
+ success: this.operationSuccess,
+ pipelineId: this.pipelineId,
+ } satisfies SavePipelineDialogResult);
}
}
diff --git a/ui/src/app/editor/editor.component.html
b/ui/src/app/editor/editor.component.html
index 38a4d76a3c..69f45223c6 100644
--- a/ui/src/app/editor/editor.component.html
+++ b/ui/src/app/editor/editor.component.html
@@ -49,6 +49,7 @@
[rawPipelineModel]="rawPipelineModel"
[allElements]="allElements"
[originalPipeline]="originalPipeline"
+ [cloneMode]="cloneMode"
[pipelineCanvasMetadata]="pipelineCanvasMetadata"
[pipelineCanvasMetadataAvailable]="
pipelineCanvasMetadataAvailable
diff --git a/ui/src/app/editor/editor.component.ts
b/ui/src/app/editor/editor.component.ts
index 5a381a5342..b831c54554 100644
--- a/ui/src/app/editor/editor.component.ts
+++ b/ui/src/app/editor/editor.component.ts
@@ -78,6 +78,7 @@ export class EditorComponent implements OnInit {
rawPipelineModel: PipelineElementConfig[] = [];
originalPipeline: Pipeline;
+ cloneMode = false;
allElementsLoaded = false;
allMetadataLoaded = false;
@@ -86,6 +87,8 @@ export class EditorComponent implements OnInit {
ngOnInit() {
const pipelineId = this.activatedRoute.snapshot.params.pipelineId;
+ this.cloneMode =
+ this.activatedRoute.snapshot.queryParamMap.get('clone') === 'true';
if (pipelineId) {
this.loadPipelineToModify(pipelineId);
} else {
@@ -153,7 +156,7 @@ export class EditorComponent implements OnInit {
this.breadcrumbService.updateBreadcrumb([
SpPipelineRoutes.BASE,
{ label: this.originalPipeline.name },
- { label: 'Modify' },
+ { label: this.cloneMode ? 'Clone' : 'Modify' },
]);
this.rawPipelineModel =
this.jsplumbService.makeRawPipeline(
this.originalPipeline,
diff --git
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
index 4fb0cb6d3f..5c9aec51d0 100644
---
a/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
+++
b/ui/src/app/pipelines/components/pipeline-overview/pipeline-overview.component.html
@@ -215,20 +215,40 @@
<mat-icon>mode_edit</mat-icon>
<span>{{ 'Edit' | translate }}</span>
</button>
+ <button
+ mat-menu-item
+ (click)="
+ pipelineOperationsService.clonePipeline(element.elementId)
+ "
+ data-cy="clone-pipeline-btn"
+ >
+ <mat-icon>content_copy</mat-icon>
+ <span>{{ 'Clone' | translate }}</span>
+ </button>
}
<button
mat-menu-item
(click)="
- pipelineOperationsService.showPermissionsDialog(
+ pipelineOperationsService.showManageDialog(
element,
refreshPipelinesEmitter
)
"
- data-cy="open-manage-permissions"
+ data-cy="open-manage-pipeline"
>
<mat-icon>share</mat-icon>
- <span>{{ 'Manage permissions' | translate }}</span>
+ <span>{{ 'Manage' | translate }}</span>
</button>
+ @if (hasPipelineWritePrivileges) {
+ <button
+ mat-menu-item
+ (click)="pipelineOperationsService.showCodeDialog(element)"
+ data-cy="open-pipeline-code-trigger"
+ >
+ <mat-icon>code</mat-icon>
+ <span>{{ 'Code' | translate }}</span>
+ </button>
+ }
@if (hasPipelineWritePrivileges) {
<button
mat-menu-item
diff --git a/ui/src/app/pipelines/services/pipeline-operations.service.ts
b/ui/src/app/pipelines/services/pipeline-operations.service.ts
index 1a2e0fa37b..a07817169c 100644
--- a/ui/src/app/pipelines/services/pipeline-operations.service.ts
+++ b/ui/src/app/pipelines/services/pipeline-operations.service.ts
@@ -17,10 +17,17 @@
*/
import { EventEmitter, inject, Injectable } from '@angular/core';
-import { PipelineSummaryDto } from '@streampipes/platform-services';
+import {
+ Message,
+ Pipeline,
+ PipelineService,
+ PipelineSummaryDto,
+} from '@streampipes/platform-services';
import {
DialogRef,
DialogService,
+ ObjectManageDialogComponent,
+ ObjectManageDialogResourceConfig,
ObjectPermissionDialogComponent,
PanelType,
} from '@streampipes/shared-ui';
@@ -29,11 +36,14 @@ import { DeletePipelineDialogComponent } from
'../dialog/delete-pipeline/delete-
import { Router } from '@angular/router';
import { PipelineAction } from '../model/pipeline-model';
import { PipelineNotificationsComponent } from
'../dialog/pipeline-notifications/pipeline-notifications.component';
+import { PipelineCodeDialogComponent } from
'../../pipeline-details/dialogs/pipeline-code/pipeline-code-dialog.component';
+import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class PipelineOperationsService {
private dialogService = inject(DialogService);
private router = inject(Router);
+ private pipelineService = inject(PipelineService);
starting: any;
stopping: any;
@@ -156,6 +166,77 @@ export class PipelineOperationsService {
});
}
+ showManageDialog(
+ pipelineSummary: PipelineSummaryDto,
+ refreshPipelinesEmitter: EventEmitter<boolean>,
+ ) {
+ this.pipelineService
+ .getPipelineById(pipelineSummary.elementId)
+ .subscribe(pipeline => {
+ const resourceConfig:
ObjectManageDialogResourceConfig<Pipeline> =
+ {
+ resourceLabel: 'Pipeline',
+ nameLabel: 'Pipeline name',
+ descriptionLabel: 'Description',
+ nameProperty: 'name',
+ assetLinkType: 'pipeline',
+ assetLinkCheckboxLabel:
+ 'Add the current pipeline to an existing asset',
+ saveResource: async resource => {
+ const shouldRestart = resource.running;
+
+ if (shouldRestart) {
+ const stopResult = await firstValueFrom(
+ this.pipelineService.stopPipeline(
+ resource._id,
+ ),
+ );
+ this.assertPipelineOperationSucceeded(
+ stopResult.success,
+ 'Stopping the pipeline failed.',
+ );
+ }
+
+ const result = await firstValueFrom(
+ this.pipelineService.updatePipeline(resource),
+ );
+ this.assertPipelineSaveSucceeded(result);
+
+ if (shouldRestart) {
+ const startResult = await firstValueFrom(
+ this.pipelineService.startPipeline(
+ resource._id,
+ ),
+ );
+ this.assertPipelineOperationSucceeded(
+ startResult.success,
+ 'Starting the pipeline failed.',
+ );
+ }
+ },
+ };
+ const dialogRef = this.dialogService.open(
+ ObjectManageDialogComponent,
+ {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: 'Manage',
+ width: '50vw',
+ data: {
+ objectInstanceId: pipeline._id,
+ resource: { ...pipeline },
+ saveMode: 'immediate',
+ resourceConfig,
+ headerTitle: 'Manage Pipeline ' + pipeline.name,
+ },
+ },
+ );
+
+ dialogRef.afterClosed().subscribe(refresh => {
+ refreshPipelinesEmitter.emit(!!refresh);
+ });
+ });
+ }
+
showPermissionsDialog(
pipelineSummary: PipelineSummaryDto,
refreshPipelinesEmitter: EventEmitter<boolean>,
@@ -180,15 +261,55 @@ export class PipelineOperationsService {
});
}
+ showCodeDialog(pipelineSummary: PipelineSummaryDto): void {
+ this.pipelineService
+ .getPipelineById(pipelineSummary.elementId)
+ .subscribe(pipeline => {
+ this.dialogService.open(PipelineCodeDialogComponent, {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ width: '50vw',
+ title: 'Pipeline code',
+ data: {
+ pipeline,
+ },
+ });
+ });
+ }
+
showPipelineInEditor(id: string) {
this.router.navigate(['pipelines', 'modify', id]);
}
+ showPipelineCloneInEditor(id: string) {
+ this.router.navigate(['pipelines', 'modify', id], {
+ queryParams: { clone: true },
+ });
+ }
+
showPipelineDetails(id: string) {
this.router.navigate(['pipelines', 'details', id]);
}
- modifyPipeline(pipeline) {
- this.showPipelineInEditor(pipeline);
+ modifyPipeline(pipelineId: string) {
+ this.showPipelineInEditor(pipelineId);
+ }
+
+ clonePipeline(pipelineId: string) {
+ this.showPipelineCloneInEditor(pipelineId);
+ }
+
+ private assertPipelineSaveSucceeded(result: Message): void {
+ if (!result.success) {
+ throw new Error('Saving the pipeline failed.');
+ }
+ }
+
+ private assertPipelineOperationSucceeded(
+ success: boolean,
+ errorMessage: string,
+ ): void {
+ if (!success) {
+ throw new Error(errorMessage);
+ }
}
}