You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ze...@apache.org on 2026/06/05 05:37:00 UTC
(streampipes) branch dev updated: feat: Workflow consistency for Dashboards (#4487)
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 6207ca9460 feat: Workflow consistency for Dashboards (#4487)
6207ca9460 is described below
commit 6207ca9460cc3249637b69f12de4231ef82ccbbe
Author: Jacqueline Höllig <78...@users.noreply.github.com>
AuthorDate: Fri Jun 5 07:36:56 2026 +0200
feat: Workflow consistency for Dashboards (#4487)
Co-authored-by: Philipp Zehnder <te...@users.noreply.github.com>
---
ui/cypress/support/utils/GeneralUtils.ts | 19 +-
ui/cypress/support/utils/chart/ChartBtns.ts | 4 +-
ui/cypress/support/utils/chart/ChartUtils.ts | 101 ++-
ui/cypress/support/utils/user/PermissionUtils.ts | 32 +-
.../tests/chart/addAssetToDashboard.smoke.spec.ts | 1 +
.../tests/chart/addAssetsToDataView.smoke.spec.ts | 1 +
ui/deployment/i18n/de.json | 5 +
ui/deployment/i18n/en.json | 5 +
ui/deployment/i18n/pl.json | 5 +
.../model/object-manage-model.model.ts | 62 ++
.../object-manage-dialog.component.html | 415 +++++++++++
.../object-manage-dialog.component.scss} | 55 +-
.../object-manage-dialog.component.ts | 820 +++++++++++++++++++++
.../streampipes/shared-ui/src/public-api.ts | 2 +
.../chart-container/chart-container.component.ts | 12 +
.../grid-view/dashboard-grid-view.component.ts | 6 +
.../dashboard-overview-table.component.html | 14 +-
.../dashboard-overview-table.component.ts | 38 +-
.../overview/dashboard-overview.component.ts | 45 +-
.../panel/dashboard-panel.component.html | 187 ++++-
.../panel/dashboard-panel.component.scss | 4 +
.../components/panel/dashboard-panel.component.ts | 329 ++++++++-
.../dashboard-toolbar.component.html | 28 +-
.../dashboard-toolbar.component.ts | 6 +
24 files changed, 2002 insertions(+), 194 deletions(-)
diff --git a/ui/cypress/support/utils/GeneralUtils.ts b/ui/cypress/support/utils/GeneralUtils.ts
index 4312da1d9b..b972c0acf0 100644
--- a/ui/cypress/support/utils/GeneralUtils.ts
+++ b/ui/cypress/support/utils/GeneralUtils.ts
@@ -20,16 +20,29 @@ export class GeneralUtils {
public static tab(identifier: string) {
return cy.dataCy(`tab-${identifier}`).click();
}
+
public static openMenuForRow(rowText: string) {
- cy.contains('[role="row"], tr, mat-row', rowText) // be flexible on row element
+ GeneralUtils.closeVisibleMaterialMenu();
+
+ cy.contains('[role="row"], tr, mat-row', rowText, {
+ timeout: 10000,
+ })
.scrollIntoView()
.within(() => {
cy.dataCy('more-options').click({ force: true });
});
- // Wait for the CDK overlay panel to become visible
+ GeneralUtils.visibleMaterialMenu().should('exist');
+ }
+
+ public static closeVisibleMaterialMenu() {
+ cy.get('body').type('{esc}', { force: true });
cy.get('.cdk-overlay-container .mat-mdc-menu-panel:visible').should(
- 'exist',
+ 'not.exist',
);
}
+
+ public static visibleMaterialMenu() {
+ return cy.get('.cdk-overlay-container .mat-mdc-menu-panel:visible');
+ }
}
diff --git a/ui/cypress/support/utils/chart/ChartBtns.ts b/ui/cypress/support/utils/chart/ChartBtns.ts
index cf51949cae..a8bf71bf80 100644
--- a/ui/cypress/support/utils/chart/ChartBtns.ts
+++ b/ui/cypress/support/utils/chart/ChartBtns.ts
@@ -26,7 +26,7 @@ export class ChartBtns {
}
public static saveDataViewBtn() {
- return cy.dataCy('save-data-view');
+ return cy.dataCy('sp-manage-save');
}
public static saveDashboardBtn() {
@@ -77,7 +77,7 @@ export class ChartBtns {
}
public static editDashboardSettingsBtn(dashboardName) {
- return cy.dataCy('edit-dashboard-settings-' + dashboardName);
+ return cy.dataCy('open-manage-permissions-' + dashboardName);
}
public static openNewDataViewBtn() {
diff --git a/ui/cypress/support/utils/chart/ChartUtils.ts b/ui/cypress/support/utils/chart/ChartUtils.ts
index 08f9ab319a..16f05b4bba 100644
--- a/ui/cypress/support/utils/chart/ChartUtils.ts
+++ b/ui/cypress/support/utils/chart/ChartUtils.ts
@@ -61,22 +61,30 @@ export class ChartUtils {
public static checkChartCanBeEdited(chartName: string) {
GeneralUtils.openMenuForRow(chartName);
- ChartBtns.editDataViewButton(chartName).should('exist');
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDataViewButton(chartName).should('exist');
+ });
}
public static checkChartCanNotBeEdited(chartName: string) {
GeneralUtils.openMenuForRow(chartName);
- ChartBtns.editDataViewButton(chartName).should('not.exist');
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDataViewButton(chartName).should('not.exist');
+ });
}
public static checkDashboardCanBeEdited(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.editDashboardBtn(dashboardName).should('exist');
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDashboardBtn(dashboardName).should('exist');
+ });
}
public static checkDashboardCanNotBeEdited(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.editDashboardBtn(dashboardName).should('not.exist');
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDashboardBtn(dashboardName).should('not.exist');
+ });
}
public static initDataLakeTests() {
@@ -139,7 +147,7 @@ export class ChartUtils {
}
public static addAssetsToDashboard(assetNameList: string[]) {
- cy.dataCy('sp-show-dashboard-asset-checkbox')
+ cy.dataCy('sp-show-asset-checkbox')
.find('input[type="checkbox"]')
.then($checkbox => {
if (!$checkbox.prop('checked')) {
@@ -168,6 +176,7 @@ export class ChartUtils {
ChartUtils.goToDashboard();
ChartUtils.addNewDashboard(name);
ChartUtils.saveDataView();
+ ChartUtils.waitForDashboardInOverview(name);
}
public static createNewDashboardWithAssetLinks(
@@ -178,11 +187,22 @@ export class ChartUtils {
ChartUtils.addNewDashboard(name);
ChartUtils.addAssetsToDashboard(assetNameList);
ChartUtils.saveDataView();
+ ChartUtils.waitForDashboardInOverview(name);
}
public static addNewDashboard(name: string) {
ChartBtns.newDashboardDialogBtn().click();
- cy.dataCy('data-view-name').type(name);
+ ChartBtns.saveDashboardBtn().click();
+ cy.dataCy('managed-resource-name').type(name);
+ ChartBtns.saveDataViewBtn().click();
+ }
+
+ public static addNewDashboardwithAssets(name: string, assetNameList) {
+ ChartBtns.newDashboardDialogBtn().click();
+ ChartBtns.saveDashboardBtn().click();
+ cy.dataCy('managed-resource-name').type(name);
+ ChartUtils.addAssetsToDashboard(assetNameList);
+ ChartBtns.saveDataViewBtn().click();
}
public static createDashboardWithLinkedAssets(
@@ -199,9 +219,8 @@ export class ChartUtils {
ChartUtils.goToDashboard();
//ADD Assets
- ChartUtils.addNewDashboard(name);
- ChartUtils.addAssetsToDashboard(assetNameList);
- ChartUtils.saveDataView();
+ ChartUtils.addNewDashboardwithAssets(name, assetNameList);
+ ChartUtils.waitForDashboardInOverview(name);
}
public static saveDataView() {
@@ -212,6 +231,14 @@ export class ChartUtils {
return ChartBtns.saveDashboardBtn().click();
}
+ public static waitForDashboardInOverview(name: string) {
+ cy.dataCy('sp-manage-save', { timeout: 10000 }).should('not.exist');
+ ChartUtils.goToDashboard();
+ cy.contains('[role="row"], tr, mat-row', name, {
+ timeout: 10000,
+ }).should('be.visible');
+ }
+
public static addDataViewAndTableWidget(
dataViewName: string,
dataSet: string,
@@ -244,8 +271,8 @@ export class ChartUtils {
}
public static renameDashboard(newName: string) {
- cy.dataCy('data-view-name').clear().type(newName);
- cy.dataCy('data-view-name').should('have.value', newName);
+ cy.dataCy('managed-resource-name').clear().type(newName);
+ cy.dataCy('managed-resource-name').should('have.value', newName);
}
public static loadRandomDataSetIntoDataLake() {
@@ -255,10 +282,13 @@ export class ChartUtils {
public static createAndEditDashboard(name: string) {
// Create new data view
ChartBtns.newDashboardDialogBtn().click();
-
+ ChartBtns.saveDashboardBtn().click();
// Configure data view
- cy.dataCy('data-view-name').type(name);
+ cy.dataCy('managed-resource-name').type(name);
ChartBtns.saveDataViewBtn().click();
+ cy.contains('[role="row"], tr, mat-row', name, {
+ timeout: 10000,
+ }).should('be.visible');
this.editDashboard(name);
}
@@ -289,24 +319,32 @@ export class ChartUtils {
public static editDashboard(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.editDashboardBtn(dashboardName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDashboardBtn(dashboardName).click();
+ });
}
public static viewDashboard(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.viewDashboardBtn(dashboardName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.viewDashboardBtn(dashboardName).click();
+ });
}
public static editDashboardSettings(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.editDashboardSettingsBtn(dashboardName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDashboardSettingsBtn(dashboardName).click();
+ });
}
public static editDataView(dataViewName: string) {
// Click edit button
// following only works if single view is available
GeneralUtils.openMenuForRow(dataViewName);
- ChartBtns.editDataViewButton(dataViewName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.editDataViewButton(dataViewName).click();
+ });
}
public static saveDataViewConfiguration(confirmSave: boolean = false) {
@@ -338,6 +376,15 @@ export class ChartUtils {
ChartBtns.confirmAssetSelectionBtn();
}
+ public static addDashboardToAsset(assetNameList = []) {
+ cy.dataCy('sp-show-asset-checkbox').then($checkbox => {
+ if (!$checkbox.is(':checked')) {
+ cy.wrap($checkbox).click({ force: true });
+ }
+ });
+ this.addToAsset(assetNameList);
+ }
+
public static addToAsset(assetNameList = []) {
cy.get('mat-tree.asset-tree', { timeout: 10000 }).should('exist');
@@ -351,25 +398,33 @@ export class ChartUtils {
public static deleteDashboard(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.deleteDashboardBtn(dashboardName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.deleteDashboardBtn(dashboardName).click();
+ });
SharedBtns.confirmDialogConfirmBtn().click();
}
public static deleteDataView(dataViewName: string) {
GeneralUtils.openMenuForRow(dataViewName);
- ChartBtns.deleteDataViewBtn(dataViewName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.deleteDataViewBtn(dataViewName).click();
+ });
SharedBtns.confirmDialogConfirmBtn().click();
}
public static cancelDeleteDashboard(dashboardName: string) {
GeneralUtils.openMenuForRow(dashboardName);
- ChartBtns.deleteDashboardBtn(dashboardName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.deleteDashboardBtn(dashboardName).click();
+ });
SharedBtns.confirmDialogCancelBtn().click();
}
public static cancelDeleteDataView(dataViewName: string) {
GeneralUtils.openMenuForRow(dataViewName);
- ChartBtns.deleteDataViewBtn(dataViewName).click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ ChartBtns.deleteDataViewBtn(dataViewName).click();
+ });
SharedBtns.confirmDialogCancelBtn().click();
}
@@ -768,8 +823,8 @@ export class ChartUtils {
ChartUtils.ADAPTER_NAME,
);
//Save
- ChartUtils.addChartsToAsset(assetNames);
- ChartUtils.saveDataViewConfiguration();
+ ChartUtils.addDashboardToAsset(assetNames);
+ ChartBtns.saveDataViewBtn(); //.saveDataViewConfiguration();
//Necessary for the background task to finish otherwise it steps back to charts from the following task
cy.wait(500);
}
diff --git a/ui/cypress/support/utils/user/PermissionUtils.ts b/ui/cypress/support/utils/user/PermissionUtils.ts
index 4365eed24f..2ee84f5860 100644
--- a/ui/cypress/support/utils/user/PermissionUtils.ts
+++ b/ui/cypress/support/utils/user/PermissionUtils.ts
@@ -22,7 +22,13 @@ import { GeneralUtils } from '../GeneralUtils';
export class PermissionUtils {
public static openManagePermissions(resourceName: string) {
GeneralUtils.openMenuForRow(resourceName);
- cy.dataCy('open-manage-permissions').click();
+ GeneralUtils.visibleMaterialMenu().within(() => {
+ cy.dataCy('open-manage-permissions-' + resourceName, {
+ timeout: 10000,
+ })
+ .should('be.visible')
+ .click();
+ });
}
public static changeOwnership(resourceName: string, email: string) {
@@ -56,17 +62,31 @@ export class PermissionUtils {
}
public static save() {
- cy.dataCy('sp-manage-permissions-save').click();
+ cy.dataCy('sp-manage-save').click();
}
public static cancel() {
- cy.dataCy('sp-manage-permissions-cancel').click();
+ cy.dataCy('sp-manage-cancel').click();
}
public static validateUserCanNotChangePermissions(resourceName: string) {
- PermissionUtils.openManagePermissions(resourceName);
- cy.dataCy('warning-permissions-managed-by-owner').should('exist');
- PermissionUtils.cancel();
+ GeneralUtils.openMenuForRow(resourceName);
+ GeneralUtils.visibleMaterialMenu().then($menu => {
+ const managePermissionsButton = $menu.find(
+ `[data-cy="open-manage-permissions-${resourceName}"]`,
+ );
+
+ if (managePermissionsButton.length > 0) {
+ cy.wrap(managePermissionsButton).should('be.visible').click();
+ cy.dataCy('warning-permissions-managed-by-owner').should(
+ 'exist',
+ );
+ PermissionUtils.cancel();
+ } else {
+ cy.wrap(managePermissionsButton).should('not.exist');
+ GeneralUtils.closeVisibleMaterialMenu();
+ }
+ });
}
public static validateUserCanChangePermissions(resourceName: string) {
diff --git a/ui/cypress/tests/chart/addAssetToDashboard.smoke.spec.ts b/ui/cypress/tests/chart/addAssetToDashboard.smoke.spec.ts
index 31888e5990..32328b080a 100644
--- a/ui/cypress/tests/chart/addAssetToDashboard.smoke.spec.ts
+++ b/ui/cypress/tests/chart/addAssetToDashboard.smoke.spec.ts
@@ -71,6 +71,7 @@ describe('Test add Assets To Dashboard', () => {
name,
assetNameList,
);
+ ChartUtils.goToDashboard();
ChartUtils.editDashboardSettings(name);
ChartUtils.renameDashboard('NEW');
const assetNameList2 = [assetName2, assetName3];
diff --git a/ui/cypress/tests/chart/addAssetsToDataView.smoke.spec.ts b/ui/cypress/tests/chart/addAssetsToDataView.smoke.spec.ts
index 532d83db6f..d2920286ca 100644
--- a/ui/cypress/tests/chart/addAssetsToDataView.smoke.spec.ts
+++ b/ui/cypress/tests/chart/addAssetsToDataView.smoke.spec.ts
@@ -47,6 +47,7 @@ describe('Creates a new adapter with a linked asset', () => {
it('Edit Assets during Chart generation', () => {
ChartUtils.createDataViewWithAssets([assetName1, assetName2]);
+
AssetUtils.checkAmountOfAssets(3);
//Test
AssetUtils.checkAmountOfLinkedResourcesByAssetName(assetName1, 1);
diff --git a/ui/deployment/i18n/de.json b/ui/deployment/i18n/de.json
index 2170fea755..0df47c3fa0 100644
--- a/ui/deployment/i18n/de.json
+++ b/ui/deployment/i18n/de.json
@@ -472,6 +472,7 @@
"Full stack trace": "Vollständiger Stack-Trace",
"Gathering data for live preview...": "Sammeln von Daten für die Live-Vorschau...",
"Gauge": "Gauge",
+ "General": "Allgemein",
"General Settings": "Allgemeine Einstellungen",
"Generate and download new key pair": "Neues Schlüsselpaar generieren und herunterladen",
"Geo features": "Geo-Features",
@@ -614,9 +615,12 @@
"Mail Settings": "E-Mail-Einstellungen",
"Mail Template": "Mail-Vorlage",
"Make available to all users with appropriate role": "Allen Benutzern mit entsprechender Rolle zur Verfügung stellen",
+ "Manage": "Verwalten",
+ "Manage Dashboard ": "Dashboard verwalten",
"Manage Labels": "Labels verwalten",
"Manage Sites": "Standorte verwalten",
"Manage asset links": "Verwalten von Asset-Verknüpfungen",
+ "Manage dashboard": "Dashboard verwalten",
"Manage datasets": "Datensätze verwalten",
"Manage links": "Verwalten von Links",
"Manage permissions": "Berechtigungen verwalten",
@@ -746,6 +750,7 @@
"Passwords does not match.": "Die Passwörter stimmen nicht überein.",
"Path": "Pfad",
"Percent": "Prozent",
+ "Permissions": "Berechtigungen",
"Persist events": "Daten speichern",
"Pie": "Kreisdiagramm",
"Pin": "Pin",
diff --git a/ui/deployment/i18n/en.json b/ui/deployment/i18n/en.json
index e8f50f39fa..ef026e3af3 100644
--- a/ui/deployment/i18n/en.json
+++ b/ui/deployment/i18n/en.json
@@ -472,6 +472,7 @@
"Full stack trace": null,
"Gathering data for live preview...": null,
"Gauge": null,
+ "General": null,
"General Settings": null,
"Generate and download new key pair": null,
"Geo features": null,
@@ -614,9 +615,12 @@
"Mail Settings": null,
"Mail Template": null,
"Make available to all users with appropriate role": null,
+ "Manage": null,
+ "Manage Dashboard ": null,
"Manage Labels": null,
"Manage Sites": null,
"Manage asset links": null,
+ "Manage dashboard": null,
"Manage datasets": null,
"Manage links": null,
"Manage permissions": null,
@@ -746,6 +750,7 @@
"Passwords does not match.": null,
"Path": null,
"Percent": null,
+ "Permissions": null,
"Persist events": null,
"Pie": null,
"Pin": null,
diff --git a/ui/deployment/i18n/pl.json b/ui/deployment/i18n/pl.json
index ce99178a64..5f996fd63c 100644
--- a/ui/deployment/i18n/pl.json
+++ b/ui/deployment/i18n/pl.json
@@ -472,6 +472,7 @@
"Full stack trace": "Pełny ślad stosu",
"Gathering data for live preview...": "Zbieranie danych do podglądu na żywo...",
"Gauge": "Wskaźnik (gauge)",
+ "General": "Ogólne",
"General Settings": "Ustawienia ogólne",
"Generate and download new key pair": "Wygeneruj i pobierz nową parę kluczy",
"Geo features": "Funkcje geograficzne",
@@ -614,9 +615,12 @@
"Mail Settings": "Ustawienia poczty",
"Mail Template": "Szablon wiadomości",
"Make available to all users with appropriate role": "Udostępnij wszystkim użytkownikom z odpowiednią rolą",
+ "Manage": "Zarządzanie",
+ "Manage Dashboard ": "Zarządzaj pulpitem nawigacyjnym",
"Manage Labels": "Zarządzaj etykietami",
"Manage Sites": "Zarządzaj lokalizacjami",
"Manage asset links": "Zarządzaj linkami zasobu",
+ "Manage dashboard": "Zarządzanie pulpitem nawigacyjnym",
"Manage datasets": "Zarządzaj zestawami danych",
"Manage links": "Zarządzaj linkami",
"Manage permissions": "Zarządzaj uprawnieniami",
@@ -746,6 +750,7 @@
"Passwords does not match.": "Hasła nie są zgodne.",
"Path": "Ścieżka",
"Percent": "Procent",
+ "Permissions": "Uprawnienia",
"Persist events": "Trwałe przechowywanie zdarzeń",
"Pie": "Wykres kołowy",
"Pin": "Pinezka",
diff --git a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/model/object-manage-model.model.ts b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/model/object-manage-model.model.ts
new file mode 100644
index 0000000000..8a1d8bf750
--- /dev/null
+++ b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/model/object-manage-model.model.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { Permission, SpAssetTreeNode } from '@streampipes/platform-services';
+import { Observable } from 'rxjs';
+
+export type ObjectManageDialogSaveMode = 'deferred' | 'immediate';
+
+export interface ObjectManageDialogResource {
+ _id?: string;
+ elementId?: string;
+ title?: string;
+ name?: string;
+ description?: string;
+ updatedAt?: number;
+ lastModified?: number;
+}
+
+export interface ObjectManageDialogResourceConfig<
+ TResource extends ObjectManageDialogResource = ObjectManageDialogResource,
+> {
+ resourceLabel?: string;
+ nameLabel?: string;
+ descriptionLabel?: string;
+ idProperty?: '_id' | 'elementId';
+ nameProperty?: 'title' | 'name';
+ descriptionProperty?: string;
+ showResourceFields?: boolean;
+ showAssetLinking?: boolean;
+ assetLinkType?: string;
+ assetLinkCheckboxLabel?: string;
+ saveResource?: (
+ resource: TResource,
+ ) => Observable<unknown> | Promise<unknown>;
+}
+
+export interface ObjectManageDialogResult<
+ TResource extends ObjectManageDialogResource = ObjectManageDialogResource,
+> {
+ resource: TResource;
+ nb?: TResource;
+ permission?: Permission;
+ selectedAssets: SpAssetTreeNode[];
+ deselectedAssets: SpAssetTreeNode[];
+ originalAssets: SpAssetTreeNode[];
+ addToAssets: boolean;
+}
diff --git a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
new file mode 100644
index 0000000000..e8ceedcf8b
--- /dev/null
+++ b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.html
@@ -0,0 +1,415 @@
+<!--
+~ 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 class="sp-dialog-container">
+ @if (loading) {
+ <div class="sp-dialog-content">
+ <div fxFlex="100" fxLayoutAlign="center center" fxLayout="column">
+ <mat-spinner [diameter]="25"></mat-spinner>
+ <h5>{{ 'Loading' | translate }}</h5>
+ </div>
+ </div>
+ } @else if (!permissionDenied || createMode) {
+ <div class="sp-dialog-content">
+ <div fxFlex="100" fxLayout="column" class="p-15">
+ <h4>{{ headerTitle }}</h4>
+ <form [formGroup]="parentForm" fxFlex="100" fxLayout="column">
+ @if (showResourceSection) {
+ <sp-split-section
+ [level]="3"
+ [title]="resourceLabel | translate"
+ >
+ @if (showResourceFields) {
+ <sp-form-field
+ [level]="3"
+ [label]="resourceNameLabel | translate"
+ >
+ <mat-form-field
+ class="w-100"
+ floatLabel="auto"
+ color="accent"
+ >
+ <input
+ required
+ matInput
+ formControlName="resourceName"
+ data-cy="managed-resource-name"
+ />
+ </mat-form-field>
+ </sp-form-field>
+ <sp-form-field
+ [level]="3"
+ [label]="
+ resourceDescriptionLabel | translate
+ "
+ >
+ <mat-form-field
+ class="w-100"
+ floatLabel="auto"
+ color="accent"
+ >
+ <input
+ matInput
+ formControlName="resourceDescription"
+ data-cy="managed-resource-description"
+ />
+ </mat-form-field>
+ </sp-form-field>
+ }
+
+ @if (isAssetAdmin && showAssetLinking) {
+ <div class="mt-10" fxLayout="column">
+ <mat-checkbox
+ [(ngModel)]="addToAssets"
+ [ngModelOptions]="{ standalone: true }"
+ color="accent"
+ data-cy="sp-show-asset-checkbox"
+ >
+ {{ assetLinkCheckboxLabel | translate }}
+ </mat-checkbox>
+
+ @if (addToAssets) {
+ <div class="mt-10">
+ <sp-asset-link-configuration
+ [isEdit]="!createMode"
+ [itemId]="resourceId"
+ (selectedAssetsChange)="
+ onSelectedAssetsChange(
+ $event
+ )
+ "
+ (deselectedAssetsChange)="
+ onDeselectedAssetsChange(
+ $event
+ )
+ "
+ (originalAssetsEmitter)="
+ onOriginalAssetsEmitted(
+ $event
+ )
+ "
+ >
+ </sp-asset-link-configuration>
+ </div>
+ }
+ </div>
+ }
+ </sp-split-section>
+ }
+
+ @if (showPermissionsSection) {
+ <sp-split-section
+ [level]="3"
+ [title]="'Permissions' | translate"
+ >
+ <sp-form-field
+ [level]="3"
+ [label]="'Owner' | translate"
+ >
+ <mat-form-field color="accent">
+ <mat-select
+ data-cy="owner-select"
+ formControlName="owner"
+ fxFlex
+ required
+ >
+ @for (user of allUsers; track user) {
+ <mat-option
+ [value]="user.principalId"
+ [attr.data-cy]="
+ 'owner-option-' + user.email
+ "
+ >
+ {{ user.email }}
+ </mat-option>
+ }
+ </mat-select>
+ </mat-form-field>
+ </sp-form-field>
+
+ <mat-checkbox
+ data-cy="permission-public-element"
+ formControlName="publicElement"
+ >
+ {{ 'Public Element' | translate }} ({{
+ 'visible to registered users' | translate
+ }})
+ </mat-checkbox>
+
+ @if (!parentForm.get('publicElement')?.value) {
+ <div>
+ <sp-form-field
+ [level]="3"
+ [label]="'Authorized Users' | translate"
+ >
+ <mat-form-field
+ color="accent"
+ data-cy="authorized-users-label"
+ >
+ <mat-chip-grid
+ #chipList
+ [attr.aria-label]="
+ 'User selection' | translate
+ "
+ >
+ @for (
+ user of grantedUserAuthorities;
+ track user
+ ) {
+ <mat-chip-row
+ selectable="true"
+ removable="true"
+ (removed)="
+ removeUser(user)
+ "
+ >
+ {{ user.email }}
+ <button matChipRemove>
+ <mat-icon
+ >cancel</mat-icon
+ >
+ </button>
+ </mat-chip-row>
+ }
+ <input
+ matInput
+ [placeholder]="
+ 'Add' | translate
+ "
+ #userInput
+ [formControl]="userCtrl"
+ [matAutocomplete]="userAuto"
+ [matChipInputFor]="chipList"
+ [matChipInputSeparatorKeyCodes]="
+ separatorKeysCodes
+ "
+ data-cy="authorized-user"
+ (matChipInputTokenEnd)="
+ addUser($event)
+ "
+ />
+ </mat-chip-grid>
+
+ <mat-autocomplete
+ #userAuto="matAutocomplete"
+ (optionSelected)="
+ userSelected($event)
+ "
+ >
+ @for (
+ user of filteredUsers$
+ | async;
+ track user
+ ) {
+ <mat-option
+ [value]="user"
+ [attr.data-cy]="
+ 'user-option-' +
+ user.email
+ "
+ >
+ {{ user.email }}
+ </mat-option>
+ }
+ </mat-autocomplete>
+ </mat-form-field>
+ </sp-form-field>
+ </div>
+
+ <div>
+ <sp-form-field
+ [level]="3"
+ [label]="
+ 'Authorized Groups' | translate
+ "
+ >
+ <mat-form-field
+ color="accent"
+ data-cy="authorized-groups-label"
+ >
+ <mat-chip-grid
+ #chipList
+ [attr.aria-label]="
+ 'Group selection'
+ | translate
+ "
+ >
+ @for (
+ group of grantedGroupAuthorities;
+ track group
+ ) {
+ <mat-chip-row
+ selectable="true"
+ removable="true"
+ (removed)="
+ removeGroup(group)
+ "
+ >
+ {{ group.groupName }}
+ <button matChipRemove>
+ <mat-icon
+ >cancel</mat-icon
+ >
+ </button>
+ </mat-chip-row>
+ }
+ <input
+ matInput
+ [placeholder]="
+ 'Add' | translate
+ "
+ data-cy="authorized-group"
+ #groupInput
+ [formControl]="groupCtrl"
+ [matAutocomplete]="
+ groupAuto
+ "
+ [matChipInputFor]="chipList"
+ [matChipInputSeparatorKeyCodes]="
+ separatorKeysCodes
+ "
+ (matChipInputTokenEnd)="
+ addGroup($event)
+ "
+ />
+ </mat-chip-grid>
+
+ <mat-autocomplete
+ #groupAuto="matAutocomplete"
+ (optionSelected)="
+ groupSelected($event)
+ "
+ >
+ @for (
+ group of filteredGroups$
+ | async;
+ track group
+ ) {
+ <mat-option
+ [attr.data-cy]="
+ 'group-option-' +
+ group.groupName
+ "
+ [value]="group"
+ >
+ {{ group.groupName }}
+ </mat-option>
+ }
+ </mat-autocomplete>
+ </mat-form-field>
+ </sp-form-field>
+ </div>
+ }
+ </sp-split-section>
+ }
+
+ @if (
+ showPermissionsSection &&
+ anonymousReadSupported &&
+ parentForm.contains('readAnonymous')
+ ) {
+ <sp-split-section
+ [level]="3"
+ [title]="'Public Link' | translate"
+ >
+ <mat-checkbox
+ data-cy="permission-anonymous-read"
+ formControlName="readAnonymous"
+ >
+ {{
+ 'Allow anonymous access through public link'
+ | translate
+ }}
+ </mat-checkbox>
+
+ @if (parentForm.get('readAnonymous')?.value) {
+ <div
+ fxLayout="row"
+ fxFlex="100"
+ class="mt-10"
+ fxLayoutGap="10px"
+ fxLayoutAlign="start center"
+ >
+ <span>{{ 'URL' | translate }}</span>
+ <span class="public-link" fxFlex>{{
+ publicLink
+ }}</span>
+ <button
+ mat-icon-button
+ color="accent"
+ [matTooltip]="'Copy' | translate"
+ [cdkCopyToClipboard]="publicLink"
+ >
+ <mat-icon>content_copy</mat-icon>
+ </button>
+ </div>
+ }
+ </sp-split-section>
+ }
+ </form>
+ </div>
+ </div>
+ } @else {
+ <div
+ class="sp-dialog-content"
+ data-cy="warning-permissions-managed-by-owner"
+ >
+ <div fxFlex="100" fxLayoutAlign="center center" fxLayout="column">
+ <mat-icon>block</mat-icon>
+ <h5>
+ {{
+ 'Only admins and owners can manage permissions for this resource.'
+ | translate
+ }}
+ </h5>
+ </div>
+ </div>
+ }
+
+ <mat-divider></mat-divider>
+
+ <div class="sp-dialog-actions">
+ <div fxLayout="row">
+ @if (!loading && (!permissionDenied || createMode)) {
+ <button
+ mat-button
+ mat-flat-button
+ color="accent"
+ (click)="save()"
+ style="margin-right: 10px"
+ [disabled]="
+ parentForm.invalid ||
+ (showPermissionsSection && !permission)
+ "
+ data-cy="sp-manage-save"
+ >
+ <i class="material-icons">save</i>
+ <span> {{ saveButtonLabel | translate }}</span>
+ </button>
+ }
+ <button
+ mat-button
+ mat-flat-button
+ class="mat-basic"
+ data-cy="sp-manage-cancel"
+ (click)="close()"
+ >
+ {{ 'Cancel' | translate }}
+ </button>
+ </div>
+ </div>
+</div>
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.scss
similarity index 56%
copy from ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
copy to ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.scss
index 127f137aa2..9925783cfb 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
+++ b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.scss
@@ -1,4 +1,4 @@
-/*!
+/*
* 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.
@@ -16,50 +16,19 @@
*
*/
-.fixed-height {
- height: 50px;
+.form-field .mat-form-field-wrapper {
+ margin-bottom: -1.25em;
}
-.m-20 {
- margin: 20px;
+.form-field .mat-form-field-infix {
+ border-top: 0;
}
-.h-100 {
- height: 100%;
-}
-
-.dashboard-grid {
- display: flex;
- flex-direction: column;
- flex: 1 1 100%;
-}
-
-.dashboard-content {
- position: relative;
-}
-
-.designer-panel-container {
- width: 100%;
- height: 100%;
-}
-
-.designer-panel {
- width: 350px;
-}
-
-.panel-toggle-button {
- position: absolute;
- top: 1rem;
- right: 0.5rem;
- z-index: 2;
- background: var(--mat-sys-surface, #fff);
- box-shadow: 0 0.125rem 0.5rem rgb(0 0 0 / 15%);
-}
-
-.panel-toggle-button-collapsed {
- right: 1rem;
-}
-
-.edit-menu-btn {
- margin-right: 5px;
+.public-link {
+ background: var(--color-bg-2);
+ border: 1px solid var(--color-bg-1);
+ padding: 10px;
+ color: var(--color-default-text);
+ margin-left: 10px;
+ margin-right: 10px;
}
diff --git a/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
new file mode 100644
index 0000000000..e87d0d1c9b
--- /dev/null
+++ b/ui/projects/streampipes/shared-ui/src/lib/dialog/object-manage-dialog/object-manage-dialog.component.ts
@@ -0,0 +1,820 @@
+/*
+ * 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 { COMMA, ENTER } from '@angular/cdk/keycodes';
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ inject,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+} from '@angular/core';
+import { DialogRef } from '../../dialog/base-dialog/dialog-ref';
+import { SplitSectionComponent } from '../../components/split-section/split-section.component';
+import { FormFieldComponent } from '../../components/form-field/form-field.component';
+import { AssetLinkConfigurationComponent } from '../../components/asset-link-configuration/asset-link-configuration.component';
+import { AssetSaveService } from '../../services/asset-configuration.service';
+import { CurrentUserService } from '../../services/current-user.service';
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ UntypedFormBuilder,
+ UntypedFormControl,
+ UntypedFormGroup,
+ Validators,
+} from '@angular/forms';
+import {
+ Group,
+ LinkageData,
+ Permission,
+ PermissionEntry,
+ PermissionsService,
+ PrincipalType,
+ ShortUserInfo,
+ SpAssetTreeNode,
+ UserGroupService,
+ UserService,
+ UserInfo,
+} from '@streampipes/platform-services';
+import {
+ MatChipGrid,
+ MatChipInput,
+ MatChipInputEvent,
+ MatChipRemove,
+ MatChipRow,
+} from '@angular/material/chips';
+import {
+ combineLatest,
+ firstValueFrom,
+ isObservable,
+ Observable,
+ shareReplay,
+ zip,
+} from 'rxjs';
+import {
+ MatAutocomplete,
+ MatAutocompleteSelectedEvent,
+ MatAutocompleteTrigger,
+} from '@angular/material/autocomplete';
+import { map, startWith } from 'rxjs/operators';
+import {
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ LayoutGapDirective,
+} from '@ngbracket/ngx-layout/flex';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { MatFormField } from '@angular/material/form-field';
+import { MatOption, MatSelect } from '@angular/material/select';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { MatIcon } from '@angular/material/icon';
+import { MatInput } from '@angular/material/input';
+import { MatButton, MatIconButton } from '@angular/material/button';
+import { MatTooltip } from '@angular/material/tooltip';
+import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
+import { MatDivider } from '@angular/material/divider';
+import { AsyncPipe } from '@angular/common';
+import { TranslatePipe } from '@ngx-translate/core';
+import {
+ ObjectManageDialogResource,
+ ObjectManageDialogResourceConfig,
+ ObjectManageDialogResult,
+ ObjectManageDialogSaveMode,
+} from './model/object-manage-model.model';
+
+@Component({
+ selector: 'sp-object-manage-dialog',
+ templateUrl: './object-manage-dialog.component.html',
+ styleUrls: ['./object-manage-dialog.component.scss'],
+ imports: [
+ FlexDirective,
+ LayoutAlignDirective,
+ LayoutDirective,
+ MatProgressSpinner,
+ FormsModule,
+ ReactiveFormsModule,
+ SplitSectionComponent,
+ FormFieldComponent,
+ MatFormField,
+ MatSelect,
+ MatOption,
+ MatCheckbox,
+ MatChipGrid,
+ MatChipRow,
+ MatChipRemove,
+ MatIcon,
+ MatInput,
+ MatAutocompleteTrigger,
+ MatChipInput,
+ MatAutocomplete,
+ LayoutGapDirective,
+ MatIconButton,
+ MatTooltip,
+ CdkCopyToClipboard,
+ MatDivider,
+ MatButton,
+ AsyncPipe,
+ TranslatePipe,
+ AssetLinkConfigurationComponent,
+ ],
+})
+export class ObjectManageDialogComponent<
+ TResource extends ObjectManageDialogResource = ObjectManageDialogResource,
+>
+ implements OnInit, AfterViewInit, OnDestroy
+{
+ separatorKeysCodes: number[] = [ENTER, COMMA];
+
+ @Input()
+ createMode: boolean = false;
+
+ @Input()
+ objectInstanceId: string;
+
+ @Input()
+ headerTitle: string;
+
+ @Input()
+ anonymousReadSupported = false;
+
+ @Input()
+ publicLink = '';
+
+ @Input()
+ resource: TResource;
+
+ @Input()
+ resourceConfig: ObjectManageDialogResourceConfig<TResource> = {};
+
+ @Input()
+ nb: TResource;
+
+ @Input()
+ selectedAssets: SpAssetTreeNode[] = [];
+
+ @Input()
+ deselectedAssets: SpAssetTreeNode[] = [];
+
+ @Input()
+ originalAssets: SpAssetTreeNode[] = [];
+
+ @Input()
+ saveMode: ObjectManageDialogSaveMode = 'immediate';
+
+ currentUser: UserInfo;
+ isAssetAdmin = false;
+ addToAssets = false;
+
+ private currentUserService = inject(CurrentUserService);
+ private assetSaveService = inject(AssetSaveService);
+
+ parentForm: UntypedFormGroup;
+
+ permission?: Permission;
+
+ owner: ShortUserInfo;
+ grantedUserAuthorities: ShortUserInfo[] = [];
+ grantedGroupAuthorities: Group[] = [];
+
+ allUsers: ShortUserInfo[];
+ allGroups: Group[];
+
+ filteredUsers$: Observable<ShortUserInfo[]>;
+ filteredGroups$: Observable<Group[]>;
+
+ loading = true;
+ permissionDenied = false;
+ private assetRestoreInterval?: ReturnType<typeof setInterval>;
+
+ @ViewChild('userInput') userInput: ElementRef<HTMLInputElement>;
+ @ViewChild('groupInput') groupInput: ElementRef<HTMLInputElement>;
+ @ViewChild(AssetLinkConfigurationComponent)
+ assetLinkConfigurationComponent?: AssetLinkConfigurationComponent;
+ userCtrl = new UntypedFormControl();
+ groupCtrl = new UntypedFormControl();
+
+ private fb = inject(UntypedFormBuilder);
+ private dialogRef = inject(
+ DialogRef<ObjectManageDialogComponent<TResource>>,
+ );
+ private permissionsService = inject(PermissionsService);
+ private userService = inject(UserService);
+ private groupService = inject(UserGroupService);
+
+ ngOnInit(): void {
+ this.resource = this.resource ?? this.nb;
+ this.objectInstanceId = this.objectInstanceId ?? this.getResourceId();
+ const nameValue = this.getResourceName();
+ const descriptionValue = this.getResourceDescription();
+ this.parentForm = this.fb.group({
+ resourceName: new UntypedFormControl(
+ nameValue,
+ this.showResourceFields
+ ? [Validators.required, Validators.pattern(/\S/)]
+ : [],
+ ),
+
+ resourceDescription: new UntypedFormControl(descriptionValue, []),
+ });
+ if (!this.createMode) {
+ this.loadUsersAndGroups();
+ } else {
+ this.loading = false;
+ }
+
+ this.currentUser = this.currentUserService.getCurrentUser();
+
+ this.isAssetAdmin = this.currentUserService.hasRole('ROLE_ASSET_ADMIN');
+
+ this.addToAssets = !this.createMode;
+ }
+
+ ngAfterViewInit(): void {
+ this.schedulePendingAssetRestore();
+ }
+
+ ngOnDestroy(): void {
+ this.clearPendingAssetRestoreInterval();
+ }
+
+ get saveButtonLabel(): string {
+ return this.saveMode === 'deferred' ? 'OK' : 'Save';
+ }
+
+ get resourceLabel(): string {
+ return this.resourceConfig.resourceLabel ?? 'Resource';
+ }
+
+ get resourceNameLabel(): string {
+ return this.resourceConfig.nameLabel ?? `${this.resourceLabel} name`;
+ }
+
+ get resourceDescriptionLabel(): string {
+ return (
+ this.resourceConfig.descriptionLabel ??
+ `${this.resourceLabel} description`
+ );
+ }
+
+ get showResourceFields(): boolean {
+ return this.resourceConfig.showResourceFields !== false;
+ }
+
+ get showAssetLinking(): boolean {
+ return this.resourceConfig.showAssetLinking !== false;
+ }
+
+ get showResourceSection(): boolean {
+ return (
+ this.showResourceFields ||
+ (this.isAssetAdmin && this.showAssetLinking)
+ );
+ }
+
+ get showPermissionsSection(): boolean {
+ return !this.createMode;
+ }
+
+ get assetLinkCheckboxLabel(): string {
+ return (
+ this.resourceConfig.assetLinkCheckboxLabel ??
+ `Add the current ${this.resourceLabel.toLowerCase()} to an existing asset`
+ );
+ }
+
+ get resourceId(): string {
+ return this.getResourceId();
+ }
+
+ onSelectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
+ this.selectedAssets = updatedAssets;
+ }
+
+ onDeselectedAssetsChange(updatedAssets: SpAssetTreeNode[]): void {
+ this.deselectedAssets = updatedAssets;
+ }
+
+ onOriginalAssetsEmitted(updatedAssets: SpAssetTreeNode[]): void {
+ this.originalAssets = updatedAssets;
+ this.schedulePendingAssetRestore();
+ }
+
+ loadUsersAndGroups() {
+ this.loading = true;
+ zip(
+ this.userService.listUsers(true),
+ this.groupService.getAllUserGroups(),
+ this.permissionsService.getPermissionsForObject(
+ this.objectInstanceId,
+ ),
+ ).subscribe(
+ results => {
+ this.allUsers = results[0];
+ this.allGroups = results[1];
+ this.processPermissions(results[2]);
+ this.permissionDenied = false;
+ this.loading = false;
+ },
+ () => {
+ this.permissionDenied = true;
+ this.loading = false;
+ },
+ );
+ }
+
+ processPermissions(permissions: Permission[]) {
+ if (permissions.length > 0) {
+ this.permission = permissions[0];
+ this.parentForm.addControl(
+ 'publicElement',
+ new UntypedFormControl(
+ this.permission.publicElement,
+ Validators.required,
+ ),
+ );
+ this.parentForm.addControl(
+ 'owner',
+ new UntypedFormControl(
+ this.permission.ownerSid,
+ Validators.required,
+ ),
+ );
+ if (this.anonymousReadSupported) {
+ this.parentForm.addControl(
+ 'readAnonymous',
+ new UntypedFormControl(this.permission.readAnonymous),
+ );
+ }
+ this.filteredUsers$ = combineLatest([
+ this.userCtrl.valueChanges.pipe(startWith(null)),
+ this.parentForm
+ .get('owner')!
+ .valueChanges.pipe(
+ startWith(this.parentForm.get('owner')!.value),
+ ),
+ ]).pipe(
+ map(([username]) => {
+ const base = this.allUsers.filter(
+ u => !this.isOwnerOrAdded(u),
+ );
+ return username ? this._filter(username) : base.slice();
+ }),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+
+ this.filteredGroups$ = this.groupCtrl.valueChanges.pipe(
+ startWith(null),
+ map((groupName: string | null) => {
+ return groupName
+ ? this._filterGroup(groupName)
+ : this.allGroups
+ .filter(g => !this.isGroupAdded(g))
+ .slice();
+ }),
+ );
+
+ this.permission.grantedAuthorities.forEach(authority => {
+ if (authority.principalType === 'GROUP') {
+ this.addGroupToSelection(authority);
+ } else {
+ this.addUserToSelection(authority);
+ }
+ });
+ }
+ }
+
+ async save() {
+ if (
+ this.parentForm.invalid ||
+ !this.resource ||
+ (this.showPermissionsSection && !this.permission)
+ ) {
+ return;
+ }
+
+ const {
+ resourceName,
+ resourceDescription,
+ owner,
+ publicElement,
+ readAnonymous,
+ } = this.parentForm.getRawValue();
+ if (this.showResourceFields) {
+ this.setResourceName(resourceName.trim());
+ this.setResourceDescription(resourceDescription.trim());
+ }
+ if (this.showPermissionsSection) {
+ this.permission.publicElement = publicElement;
+ if (this.anonymousReadSupported) {
+ this.permission.readAnonymous = readAnonymous || false;
+ }
+ if (this.permission.publicElement) {
+ this.permission.grantedAuthorities = [];
+ this.grantedGroupAuthorities = [];
+ this.grantedUserAuthorities = [];
+ }
+ if (owner) {
+ this.permission.ownerSid = owner;
+ }
+
+ this.permission.grantedAuthorities = this.grantedUserAuthorities
+ .map(u => {
+ return {
+ principalType: u.principalType as PrincipalType,
+ sid: u.principalId,
+ };
+ })
+ .concat(
+ this.grantedGroupAuthorities.map(g => {
+ return { principalType: 'GROUP', sid: g.groupId };
+ }),
+ );
+ }
+ const result: ObjectManageDialogResult<TResource> = {
+ resource: this.resource,
+ nb: this.nb,
+ permission: this.permission,
+ selectedAssets: this.selectedAssets,
+ deselectedAssets: this.deselectedAssets,
+ originalAssets: this.originalAssets,
+ addToAssets: this.addToAssets,
+ };
+
+ if (this.saveMode === 'deferred') {
+ this.close(result);
+ return;
+ }
+
+ await this.saveImmediately(result);
+ this.close(true);
+ }
+
+ close(result?: ObjectManageDialogResult<TResource> | boolean) {
+ this.dialogRef.close(result);
+ }
+
+ removeUser(user: ShortUserInfo) {
+ const currentIndex = this.grantedUserAuthorities.findIndex(
+ u => u.principalId === user.principalId,
+ );
+ this.grantedUserAuthorities.splice(currentIndex, 1);
+ this.userCtrl.setValue(null);
+ }
+
+ removeGroup(group: Group) {
+ const currentIndex = this.grantedGroupAuthorities.findIndex(
+ u => u.groupId === group.groupId,
+ );
+ this.grantedGroupAuthorities.splice(currentIndex, 1);
+ this.groupCtrl.setValue(null);
+ }
+
+ addUser(event: MatChipInputEvent) {
+ event.chipInput.clear();
+ this.userCtrl.setValue(null);
+ }
+
+ addGroup(event: MatChipInputEvent) {
+ event.chipInput.clear();
+ this.groupCtrl.setValue(null);
+ }
+
+ userSelected(event: MatAutocompleteSelectedEvent) {
+ this.grantedUserAuthorities.push(event.option.value);
+ this.userInput.nativeElement.value = '';
+ this.userCtrl.setValue(null);
+ }
+
+ groupSelected(event: MatAutocompleteSelectedEvent) {
+ this.grantedGroupAuthorities.push(event.option.value);
+ this.groupInput.nativeElement.value = '';
+ this.groupCtrl.setValue(null);
+ }
+
+ private addUserToSelection(authority: PermissionEntry) {
+ const user = this.allUsers.find(u => u.principalId === authority.sid);
+ if (user !== undefined) {
+ this.grantedUserAuthorities.push(user);
+ }
+ }
+
+ private async saveImmediately(
+ result: ObjectManageDialogResult<TResource>,
+ ): Promise<void> {
+ this.touchResource(result.resource);
+ await this.saveResource(result.resource);
+ if (result.permission) {
+ await firstValueFrom(
+ this.permissionsService.updatePermission(result.permission),
+ );
+ }
+
+ if (this.shouldSaveAssetLinks(result)) {
+ await this.assetSaveService.saveSelectedAssets(
+ result.selectedAssets,
+ this.createLinkageData(result.resource),
+ result.deselectedAssets,
+ result.originalAssets,
+ );
+ }
+ }
+
+ private createLinkageData(resource: TResource): LinkageData[] {
+ const resourceId = this.getResourceId(resource);
+
+ return [
+ {
+ type: this.resourceConfig.assetLinkType ?? 'resource',
+ id: resourceId,
+ name: this.getResourceName(resource) || resourceId,
+ },
+ ];
+ }
+
+ private getResourceId(resource: TResource = this.resource): string {
+ if (!resource) {
+ return '';
+ }
+
+ const idProperty = this.getResourceIdProperty(resource);
+ return String(this.getResourceValue(resource, idProperty) ?? '');
+ }
+
+ private getResourceIdProperty(resource: TResource): '_id' | 'elementId' {
+ return (
+ this.resourceConfig.idProperty ??
+ (resource._id !== undefined ? '_id' : 'elementId')
+ );
+ }
+
+ private async saveResource(resource: TResource): Promise<void> {
+ const saveResource = this.resourceConfig.saveResource;
+ if (!saveResource) {
+ return;
+ }
+
+ const saveResult = saveResource(resource);
+ if (isObservable(saveResult)) {
+ await firstValueFrom(saveResult);
+ } else {
+ await saveResult;
+ }
+ }
+
+ private getResourceName(resource: TResource = this.resource): string {
+ if (!resource) {
+ return '';
+ }
+
+ const nameProperty = this.getResourceNameProperty(resource);
+ return String(this.getResourceValue(resource, nameProperty) ?? '');
+ }
+
+ private setResourceName(name: string): void {
+ this.setResourceValue(
+ this.resource,
+ this.getResourceNameProperty(this.resource),
+ name,
+ );
+ }
+
+ private getResourceNameProperty(resource: TResource): 'title' | 'name' {
+ return (
+ this.resourceConfig.nameProperty ??
+ (resource.title !== undefined ? 'title' : 'name')
+ );
+ }
+
+ private getResourceDescription(
+ resource: TResource = this.resource,
+ ): string {
+ if (!resource) {
+ return '';
+ }
+
+ return String(
+ this.getResourceValue(
+ resource,
+ this.getResourceDescriptionProperty(),
+ ) ?? '',
+ );
+ }
+
+ private setResourceDescription(description: string): void {
+ this.setResourceValue(
+ this.resource,
+ this.getResourceDescriptionProperty(),
+ description,
+ );
+ }
+
+ private getResourceDescriptionProperty(): string {
+ return this.resourceConfig.descriptionProperty ?? 'description';
+ }
+
+ private getResourceValue(
+ resource: TResource,
+ propertyName: string,
+ ): unknown {
+ return (resource as Record<string, unknown>)[propertyName];
+ }
+
+ private setResourceValue(
+ resource: TResource,
+ propertyName: string,
+ value: unknown,
+ ): void {
+ (resource as Record<string, unknown>)[propertyName] = value;
+ }
+
+ private touchResource(resource: TResource): void {
+ const now = Date.now();
+ if ('updatedAt' in resource) {
+ resource.updatedAt = now;
+ } else if ('lastModified' in resource) {
+ resource.lastModified = now;
+ }
+ }
+
+ private shouldSaveAssetLinks(
+ result: ObjectManageDialogResult<TResource>,
+ ): boolean {
+ return (
+ this.isAssetAdmin &&
+ result.addToAssets &&
+ (result.selectedAssets.length > 0 ||
+ result.deselectedAssets.length > 0 ||
+ result.originalAssets.length > 0)
+ );
+ }
+
+ private restorePendingAssetSelection(): boolean {
+ const assetLinkConfiguration = this.assetLinkConfigurationComponent;
+ if (
+ !assetLinkConfiguration ||
+ !this.hasPendingAssetChanges() ||
+ !assetLinkConfiguration.assetsData?.length
+ ) {
+ return false;
+ }
+
+ const assetNodeLookup = new Map(
+ this.flattenAssetNodes(assetLinkConfiguration.assetsData ?? []).map(
+ node => [this.getAssetNodeKey(node), node],
+ ),
+ );
+
+ const restoredSelectedAssets = this.mapPendingAssetsToTreeNodes(
+ this.selectedAssets,
+ assetNodeLookup,
+ );
+ const restoredDeselectedAssets = this.mapPendingAssetsToTreeNodes(
+ this.deselectedAssets,
+ assetNodeLookup,
+ );
+ const deselectedAssetKeys = new Set(
+ restoredDeselectedAssets.map(asset => this.getAssetNodeKey(asset)),
+ );
+ const selectedAssetsForDisplay = this.mergeAssetNodes(
+ assetLinkConfiguration.selectedAssets ?? [],
+ restoredSelectedAssets,
+ ).filter(
+ asset => !deselectedAssetKeys.has(this.getAssetNodeKey(asset)),
+ );
+
+ this.selectedAssets = restoredSelectedAssets;
+ this.deselectedAssets = restoredDeselectedAssets;
+ assetLinkConfiguration.selectedAssets = selectedAssetsForDisplay;
+ assetLinkConfiguration.deselectedAssets = restoredDeselectedAssets;
+ return true;
+ }
+
+ private schedulePendingAssetRestore(): void {
+ this.clearPendingAssetRestoreInterval();
+
+ if (!this.hasPendingAssetChanges()) {
+ return;
+ }
+
+ if (this.restorePendingAssetSelection()) {
+ return;
+ }
+
+ this.assetRestoreInterval = setInterval(() => {
+ if (this.restorePendingAssetSelection()) {
+ this.clearPendingAssetRestoreInterval();
+ }
+ }, 100);
+ }
+
+ private clearPendingAssetRestoreInterval(): void {
+ if (this.assetRestoreInterval) {
+ clearInterval(this.assetRestoreInterval);
+ this.assetRestoreInterval = undefined;
+ }
+ }
+
+ private hasPendingAssetChanges(): boolean {
+ return (
+ this.selectedAssets.length > 0 || this.deselectedAssets.length > 0
+ );
+ }
+
+ private mapPendingAssetsToTreeNodes(
+ assets: SpAssetTreeNode[],
+ assetNodeLookup: Map<string, SpAssetTreeNode>,
+ ): SpAssetTreeNode[] {
+ return assets.map(asset => {
+ return assetNodeLookup.get(this.getAssetNodeKey(asset)) ?? asset;
+ });
+ }
+
+ private flattenAssetNodes(nodes: SpAssetTreeNode[]): SpAssetTreeNode[] {
+ return nodes.flatMap(node => [
+ node,
+ ...this.flattenAssetNodes(node.assets ?? []),
+ ]);
+ }
+
+ private mergeAssetNodes(
+ currentAssets: SpAssetTreeNode[],
+ pendingAssets: SpAssetTreeNode[],
+ ): SpAssetTreeNode[] {
+ const mergedAssets = new Map<string, SpAssetTreeNode>();
+ [...currentAssets, ...pendingAssets].forEach(asset => {
+ mergedAssets.set(this.getAssetNodeKey(asset), asset);
+ });
+
+ return Array.from(mergedAssets.values());
+ }
+
+ private getAssetNodeKey(node: SpAssetTreeNode): string {
+ return node.assetId || node.spAssetModelId;
+ }
+
+ private addGroupToSelection(authority: PermissionEntry) {
+ const group = this.allGroups.find(u => u.groupId === authority.sid);
+ this.grantedGroupAuthorities.push(group);
+ }
+
+ private _filter(value: any): ShortUserInfo[] {
+ const isUserAccount = value instanceof ShortUserInfo;
+ const filterValue = isUserAccount
+ ? value.email.toLowerCase()
+ : value.toLowerCase();
+ return this.allUsers.filter(u => {
+ return (
+ u.email.toLowerCase().startsWith(filterValue) &&
+ !this.isOwnerOrAdded(u)
+ );
+ });
+ }
+
+ private _filterGroup(value: any): Group[] {
+ const isGroup = value instanceof Group;
+ const filterValue = isGroup
+ ? value.groupName.toLowerCase()
+ : value.toLowerCase();
+ return this.allGroups.filter(g => {
+ return (
+ g.groupName.toLowerCase().startsWith(filterValue) &&
+ !this.isGroupAdded(g)
+ );
+ });
+ }
+
+ private isOwnerOrAdded(user: ShortUserInfo): boolean {
+ return (
+ this.parentForm.get('owner').getRawValue() === user.principalId ||
+ this.grantedUserAuthorities.find(
+ authority => authority.principalId === user.principalId,
+ ) !== undefined
+ );
+ }
+
+ private isGroupAdded(group: Group): boolean {
+ return (
+ this.grantedGroupAuthorities.find(
+ authority => authority.groupId === group.groupId,
+ ) !== undefined
+ );
+ }
+}
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts b/ui/projects/streampipes/shared-ui/src/public-api.ts
index 8e9279042f..2df6df6169 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -32,6 +32,8 @@ export * from './lib/dialog/panel-dialog/panel-dialog.component';
export * from './lib/dialog/standard-dialog/standard-dialog.component';
export * from './lib/dialog/pipeline-element-help/pipeline-element-help.component';
export * from './lib/dialog/object-permission-dialog/object-permission-dialog.component';
+export * from './lib/dialog/object-manage-dialog/object-manage-dialog.component';
+export * from './lib/dialog/object-manage-dialog/model/object-manage-model.model';
export * from './lib/components/asset-browser/asset-browser-toolbar/asset-browser-toolbar.component';
export * from './lib/components/basic-header-title/header-title.component';
diff --git a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
index 1bf1833485..702ba7e6ff 100644
--- a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
+++ b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts
@@ -236,6 +236,18 @@ export class ChartContainerComponent
this.componentRef.instance.widgetIndex =
changes.widgetIndex.currentValue;
}
+ if (changes.dashboardChartOverrides && this.componentRef?.instance) {
+ this.componentRef.instance.dashboardChartOverrides =
+ changes.dashboardChartOverrides.currentValue;
+ (this.componentRef.instance as any).refreshView?.();
+ }
+ if (
+ (changes.globalTimeEnabled || changes.timeSettings) &&
+ this.componentRef?.instance
+ ) {
+ this.componentRef.instance.timeSettings = this.getTimeSettings();
+ (this.componentRef.instance as any).updateData?.();
+ }
}
ngOnInit(): void {
diff --git a/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.ts b/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.ts
index e37fc3904e..91ba2a4c98 100644
--- a/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.ts
+++ b/ui/src/app/dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component.ts
@@ -92,6 +92,12 @@ export class DashboardGridViewComponent
}
}
+ updateDashboardLayout(): void {
+ this.gridOptions.column = this.dashboard.gridColumns;
+ this.gridOptions.cellHeight = this.getGridCellHeight();
+ this.grid?.updateOptions(this.gridOptions);
+ }
+
onGridChange(data: nodesCB): void {
data.nodes.forEach(changed => {
const widget = this.dashboard.widgets.find(
diff --git a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
index 97435c4079..dc8aefe950 100644
--- a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
+++ b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.html
@@ -133,22 +133,14 @@
<button
mat-menu-item
[attr.data-cy]="
- 'edit-dashboard-settings-' + element.name
+ 'open-manage-permissions-' + element.name
"
- (click)="openEditDashboardDialog(element)"
+ (click)="showManageDialog(element)"
>
<mat-icon>settings</mat-icon>
- <span>{{ 'Settings' | translate }}</span>
+ <span>{{ 'Manage' | translate }}</span>
</button>
}
- <button
- mat-menu-item
- data-cy="open-manage-permissions"
- (click)="showPermissionsDialog(element)"
- >
- <mat-icon>share</mat-icon>
- <span>{{ 'Manage permissions' | translate }}</span>
- </button>
@if (hasDashboardWritePrivileges) {
<button
mat-menu-item
diff --git a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
index 441df8c43f..94119f19d8 100644
--- a/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
+++ b/ui/src/app/dashboard/components/overview/dashboard-overview-table/dashboard-overview-table.component.ts
@@ -43,6 +43,8 @@ import {
SpBasicHeaderTitleComponent,
SpTableActionsDirective,
SpTableComponent,
+ ObjectManageDialogResourceConfig,
+ ObjectManageDialogComponent,
} from '@streampipes/shared-ui';
import { MatDialog } from '@angular/material/dialog';
import { DataExplorerDashboardService } from '../../../../dashboard-shared/services/dashboard.service';
@@ -145,16 +147,32 @@ export class DashboardOverviewTableComponent implements OnInit, OnDestroy {
this.getDashboards();
}
- showPermissionsDialog(dashboard: Dashboard) {
- const dialogRef = this.dataExplorerSharedService.openPermissionsDialog(
- dashboard.elementId,
- this.translateService.instant(
- `Manage permissions for dashboard ${dashboard.name}`,
- ),
- true,
- this.makeDashboardKioskUrl(dashboard.elementId),
- );
-
+ showManageDialog(resource: Dashboard) {
+ const resourceConfig: ObjectManageDialogResourceConfig<Dashboard> = {
+ resourceLabel: 'Dashboard',
+ nameLabel: 'Dashboard title',
+ descriptionLabel: 'Dashboard description',
+ nameProperty: 'name',
+ assetLinkType: 'dashboard',
+ assetLinkCheckboxLabel:
+ 'Add the current dashboard to an existing asset',
+ saveResource: resource =>
+ this.dashboardService.updateDashboard(resource),
+ };
+ const dialogRef = this.dialogService.open(ObjectManageDialogComponent, {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('Manage'),
+ width: '50vw',
+ data: {
+ objectInstanceId: resource.elementId,
+ resource: { ...resource },
+ saveMode: 'immediate',
+ resourceConfig,
+ headerTitle:
+ this.translateService.instant('Manage Dashboard ') +
+ resource.name,
+ },
+ });
dialogRef.afterClosed().subscribe(refresh => {
if (refresh) {
this.getDashboards();
diff --git a/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts b/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
index dba612fa9a..b1e4731dee 100644
--- a/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
+++ b/ui/src/app/dashboard/components/overview/dashboard-overview.component.ts
@@ -17,7 +17,6 @@
*/
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { MatDialog } from '@angular/material/dialog';
import {
CurrentUserService,
SpBasicViewComponent,
@@ -26,10 +25,8 @@ import {
import { AuthService } from '../../../services/auth.service';
import { UserPrivilege } from '../../../core/auth/user-privilege.enum';
import { SpDashboardRoutes } from '../../dashboard.breadcrumb';
-import { Dashboard } from '@streampipes/platform-services';
-import { DataExplorerDashboardService } from '../../../dashboard-shared/services/dashboard.service';
import { DashboardOverviewTableComponent } from './dashboard-overview-table/dashboard-overview-table.component';
-import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+import { TranslatePipe } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { MatButton } from '@angular/material/button';
import {
@@ -37,6 +34,7 @@ import {
LayoutAlignDirective,
LayoutDirective,
} from '@ngbracket/ngx-layout/flex';
+import { ChartRoutingService } from '../../../chart-shared/services/chart-routing.service';
@Component({
selector: 'sp-dashboard-overview',
@@ -59,12 +57,10 @@ export class DashboardOverviewComponent implements OnInit, OnDestroy {
@ViewChild(DashboardOverviewTableComponent)
dashboardOverview: DashboardOverviewTableComponent;
- public dialog = inject(MatDialog);
- private dataExplorerDashboardService = inject(DataExplorerDashboardService);
private authService = inject(AuthService);
private currentUserService = inject(CurrentUserService);
private breadcrumbService = inject(SpBreadcrumbService);
- private translateService = inject(TranslateService);
+ private routingService = inject(ChartRoutingService);
private user$: Subscription;
@@ -80,40 +76,7 @@ export class DashboardOverviewComponent implements OnInit, OnDestroy {
}
openNewDashboardDialog() {
- const dataViewDashboard: Dashboard = {
- dashboardGeneralSettings: {
- chartOverrides: {
- hideToolbox: false,
- },
- },
- widgets: [],
- name: '',
- dashboardLiveSettings: {
- refreshModeActive: false,
- refreshIntervalInSeconds: 10,
- label: this.translateService.instant('Off'),
- },
- metadata: {
- createdAtEpochMs: Date.now(),
- lastModifiedEpochMs: Date.now(),
- },
- gridColumns: 12,
- };
- dataViewDashboard.dashboardGeneralSettings.gridRowHeightPx = 90;
-
- this.openDashboardModificationDialog(true, dataViewDashboard);
- }
-
- openDashboardModificationDialog(createMode: boolean, dashboard: Dashboard) {
- const dialogRef =
- this.dataExplorerDashboardService.openDashboardModificationDialog(
- createMode,
- dashboard,
- );
-
- dialogRef.afterClosed().subscribe(() => {
- this.dashboardOverview.getDashboards();
- });
+ this.routingService.navigateToDashboard(true, 'create');
}
ngOnDestroy() {
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
index d524dee1cd..bc24b024d0 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.html
@@ -30,12 +30,14 @@
fxFlex="100"
[dashboard]="dashboard"
[editMode]="editMode"
+ [createMode]="createMode"
[(viewMode)]="viewMode"
[hasDashboardWritePrivileges]="hasDashboardWritePrivileges"
[timeRangeVisible]="timeRangeVisible"
[timeSettings]="timeSettings"
(saveDashboardEmitter)="persistDashboardChanges()"
(discardDashboardEmitter)="discardChanges()"
+ (manageDashboardEmitter)="manageDashboard()"
(deleteDashboardEmitter)="deleteDashboard()"
(triggerEditModeEmitter)="triggerEditMode()"
(updateDateRangeEmitter)="updateDateRange($event)"
@@ -75,13 +77,188 @@
>
<div fxLayout="column" fxFlex="100">
@if (editMode) {
- <sp-dashboard-chart-selection-panel
- (addChartEmitter)="
- addChartToDashboard($event)
- "
+ <div
fxFlex="100"
+ class="designer-panel-content"
+ fxLayout="column"
>
- </sp-dashboard-chart-selection-panel>
+ <div
+ fxFlex="100"
+ fxLayout="column"
+ class="no-overflow"
+ >
+ <mat-tab-group
+ color="accent"
+ fxFlexFill
+ class="small-tab-headers mat-tab-fill-height"
+ [(selectedIndex)]="
+ selectedDesignerTabIndex
+ "
+ >
+ <mat-tab
+ data-cy="designer-panel-data-config"
+ [label]="'Charts' | translate"
+ >
+ <div class="scroll-tab-content">
+ <sp-chart-selection
+ (addChartEmitter)="
+ addChartToDashboard(
+ $event
+ )
+ "
+ fxFlex="100"
+ >
+ </sp-chart-selection>
+ </div>
+ </mat-tab>
+ <mat-tab
+ data-cy="designer-panel-dashboard-config"
+ [label]="'General' | translate"
+ >
+ <div
+ class="scroll-tab-content p-15"
+ fxLayout="column"
+ fxLayoutGap="10px"
+ >
+ <sp-form-field
+ [level]="3"
+ [label]="
+ 'Default view mode'
+ | translate
+ "
+ >
+ <mat-radio-group
+ [(ngModel)]="
+ dashboard
+ .dashboardGeneralSettings
+ .defaultViewMode
+ "
+ (ngModelChange)="
+ onDefaultViewModeChange(
+ $event
+ )
+ "
+ >
+ <mat-radio-button
+ class="view-radio-button"
+ [value]="'grid'"
+ >
+ {{
+ 'Grid view'
+ | translate
+ }}
+ </mat-radio-button>
+ <mat-radio-button
+ class="view-radio-button"
+ [value]="
+ 'slide'
+ "
+ >
+ {{
+ 'Slide view'
+ | translate
+ }}
+ </mat-radio-button>
+ </mat-radio-group>
+ </sp-form-field>
+
+ <sp-form-field
+ [level]="3"
+ [label]="
+ 'Grid columns'
+ | translate
+ "
+ >
+ <mat-form-field
+ floatLabel="auto"
+ color="accent"
+ >
+ <input
+ required
+ matInput
+ type="number"
+ data-cy="grid-columns"
+ [(ngModel)]="
+ dashboard.gridColumns
+ "
+ (ngModelChange)="
+ onGridLayoutSettingsChange()
+ "
+ />
+ </mat-form-field>
+ </sp-form-field>
+
+ <sp-form-field
+ [level]="3"
+ [label]="
+ 'Grid row height (px)'
+ | translate
+ "
+ >
+ <mat-form-field
+ floatLabel="auto"
+ color="accent"
+ >
+ <input
+ matInput
+ type="number"
+ min="40"
+ max="200"
+ step="5"
+ data-cy="grid-row-height"
+ [(ngModel)]="
+ dashboard
+ .dashboardGeneralSettings
+ .gridRowHeightPx
+ "
+ (ngModelChange)="
+ onGridLayoutSettingsChange()
+ "
+ />
+ </mat-form-field>
+ </sp-form-field>
+
+ <div
+ class="mt-10"
+ fxLayout="column"
+ >
+ <mat-checkbox
+ [(ngModel)]="
+ dashboard
+ .dashboardGeneralSettings
+ .globalTimeEnabled
+ "
+ (ngModelChange)="
+ onGlobalTimeEnabledChange()
+ "
+ >
+ {{
+ 'Use global time settings instead of chart time settings'
+ | translate
+ }}
+ </mat-checkbox>
+ <mat-checkbox
+ [(ngModel)]="
+ dashboard
+ .dashboardGeneralSettings
+ .chartOverrides
+ .hideToolbox
+ "
+ (ngModelChange)="
+ onChartOverridesChange()
+ "
+ >
+ {{
+ 'Hide chart toolboxes in this dashboard'
+ | translate
+ }}
+ </mat-checkbox>
+ </div>
+ </div>
+ </mat-tab>
+ </mat-tab-group>
+ </div>
+ </div>
}
</div>
</mat-drawer>
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss b/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
index 127f137aa2..3708772e94 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
@@ -63,3 +63,7 @@
.edit-menu-btn {
margin-right: 5px;
}
+
+.view-radio-button {
+ margin: 5px;
+}
diff --git a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
index b508838153..6340d3a498 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.ts
@@ -17,7 +17,14 @@
*/
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { Observable, of, Subscription, timer } from 'rxjs';
+import {
+ firstValueFrom,
+ from,
+ Observable,
+ of,
+ Subscription,
+ timer,
+} from 'rxjs';
import { DashboardGridViewComponent } from '../../../dashboard-shared/components/chart-view/grid-view/dashboard-grid-view.component';
import {
ClientDashboardItem,
@@ -26,6 +33,8 @@ import {
DashboardService,
DataExplorerWidgetModel,
DataLakeMeasure,
+ LinkageData,
+ PermissionsService,
TimeSelectionConstants,
TimeSettings,
} from '@streampipes/platform-services';
@@ -41,14 +50,21 @@ import {
ConfirmDialogAction,
ConfirmDialogComponent,
CurrentUserService,
+ DialogService,
+ FormFieldComponent,
KeyboardShortcutService,
+ ObjectManageDialogComponent,
+ ObjectManageDialogResourceConfig,
+ ObjectManageDialogResult,
+ PanelType,
ShortcutRegistration,
SpBasicViewComponent,
SpBreadcrumbService,
TimeSelectionService,
+ AssetSaveService,
} from '@streampipes/shared-ui';
import { MatDialog } from '@angular/material/dialog';
-import { catchError, map, switchMap } from 'rxjs/operators';
+import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { SpDashboardRoutes } from '../../dashboard.breadcrumb';
import { ChartRoutingService } from '../../../chart-shared/services/chart-routing.service';
import { ChartDetectChangesService } from '../../../chart/services/chart-detect-changes.service';
@@ -62,33 +78,49 @@ import {
MatDrawerContainer,
MatDrawerContent,
} from '@angular/material/sidenav';
-import { MatIcon } from '@angular/material/icon';
-import { MatIconButton } from '@angular/material/button';
-import { MatTooltip } from '@angular/material/tooltip';
-import { ChartSelectionPanelComponent } from './chart-selection-panel/chart-selection-panel.component';
import {
+ FlexFillDirective,
FlexDirective,
LayoutAlignDirective,
LayoutDirective,
+ LayoutGapDirective,
} from '@ngbracket/ngx-layout/flex';
+import { MatTab, MatTabGroup } from '@angular/material/tabs';
+import { ChartSelectionComponent } from './chart-selection-panel/chart-selection/chart-selection.component';
+import { MatFormField, MatError } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { FormsModule } from '@angular/forms';
+import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
+import { MatCheckbox } from '@angular/material/checkbox';
@Component({
selector: 'sp-dashboard-panel',
templateUrl: './dashboard-panel.component.html',
- styleUrls: ['./dashboard-panel.component.scss'],
+ styleUrls: [
+ './dashboard-panel.component.scss',
+ '../../../chart/components/chart-view/designer-panel/chart-designer-panel.component.scss',
+ ],
imports: [
SpBasicViewComponent,
FlexDirective,
+ FlexFillDirective,
LayoutDirective,
LayoutAlignDirective,
+ LayoutGapDirective,
DashboardToolbarComponent,
MatDrawerContainer,
MatDrawer,
- MatIcon,
- MatIconButton,
- MatTooltip,
- ChartSelectionPanelComponent,
MatDrawerContent,
+ MatTabGroup,
+ MatTab,
+ ChartSelectionComponent,
+ FormFieldComponent,
+ MatFormField,
+ MatInput,
+ FormsModule,
+ MatRadioGroup,
+ MatRadioButton,
+ MatCheckbox,
DashboardGridViewComponent,
DashboardSlideViewComponent,
TranslatePipe,
@@ -109,9 +141,11 @@ export class DashboardPanelComponent
timeSettings: TimeSettings;
viewMode = 'grid';
+ createMode = false;
editMode = false;
chartSelectionPanelExpanded = false;
timeRangeVisible = true;
+ selectedDesignerTabIndex = 0;
_dashboardGrid: DashboardGridViewComponent;
_dashboardSlide: DashboardSlideViewComponent;
@@ -128,6 +162,9 @@ export class DashboardPanelComponent
private shortcutService = inject(KeyboardShortcutService);
private detectChangesService = inject(ChartDetectChangesService);
private dialog = inject(MatDialog);
+ private dialogService = inject(DialogService);
+ private assetSaveService = inject(AssetSaveService);
+ private permissionsService = inject(PermissionsService);
private timeSelectionService = inject(TimeSelectionService);
private authService = inject(AuthService);
private currentUserService = inject(CurrentUserService);
@@ -142,6 +179,8 @@ export class DashboardPanelComponent
observableGenerator =
this.dataExplorerSharedService.defaultObservableGenerator();
+ private pendingManageDashboardResult?: ObjectManageDialogResult<Dashboard>;
+
public ngOnInit() {
this.shortcutReg = this.shortcutService.register('dashboard-panel', [
{ key: 'e', action: () => this.onShortcutEdit() },
@@ -159,13 +198,21 @@ export class DashboardPanelComponent
const startTime = params.startTime;
const endTime = params.endTime;
- this.getDashboard(params.id, startTime, endTime);
+ this.createMode = params.id === 'create';
+ if (this.createMode) {
+ this.initializeNewDashboard();
+ } else {
+ this.getDashboard(params.id, startTime, endTime);
+ }
this.auth$ = this.currentUserService.user$.subscribe(_ => {
this.hasDashboardWritePrivileges = this.authService.hasRole(
UserPrivilege.PRIVILEGE_WRITE_DASHBOARD,
);
- if (queryParams.editMode && this.hasDashboardWritePrivileges) {
+ if (
+ (this.createMode || queryParams.editMode) &&
+ this.hasDashboardWritePrivileges
+ ) {
this.triggerEditMode();
}
});
@@ -210,6 +257,25 @@ export class DashboardPanelComponent
});
}
+ onDefaultViewModeChange(viewMode: string): void {
+ this.viewMode = viewMode;
+ this.dashboard.dashboardGeneralSettings.defaultViewMode = viewMode;
+ }
+
+ onGridLayoutSettingsChange(): void {
+ this.dashboardGrid?.updateDashboardLayout();
+ }
+
+ onGlobalTimeEnabledChange(): void {
+ this.updateDateRange(this.timeSettings);
+ }
+
+ onChartOverridesChange(): void {
+ this.dashboard.dashboardGeneralSettings.chartOverrides = {
+ ...this.dashboard.dashboardGeneralSettings.chartOverrides,
+ };
+ }
+
private getNextWidgetY(): number {
if (!this.dashboard?.widgets?.length) {
return 0;
@@ -227,29 +293,110 @@ export class DashboardPanelComponent
.dashboardTimeSettings as TimeSettings;
const currentTimeSettings = this.dashboard
.dashboardTimeSettings as TimeSettings;
- return this.detectChangesService.shouldShowConfirm(
- this.originalDashboard,
- this.dashboard,
- originalTimeSettings,
- currentTimeSettings,
- model => {
- model.dashboardTimeSettings = undefined;
- },
+ return (
+ this.pendingManageDashboardResult !== undefined ||
+ this.detectChangesService.shouldShowConfirm(
+ this.originalDashboard,
+ this.dashboard,
+ originalTimeSettings,
+ currentTimeSettings,
+ model => {
+ model.dashboardTimeSettings = undefined;
+ },
+ )
);
}
persistDashboardChanges() {
this.dashboard.dashboardGeneralSettings.defaultViewMode = this.viewMode;
- this.dashboard.metadata ??= {
- createdAtEpochMs: undefined,
- lastModifiedEpochMs: undefined,
+ if (this.createMode) {
+ this.openCreateDashboardDialog();
+ return;
+ }
+ this.saveDashboardChanges().subscribe(() => {
+ this.routingService.navigateToDashboardOverview(true);
+ });
+ }
+
+ private openCreateDashboardDialog(): void {
+ const resourceConfig: ObjectManageDialogResourceConfig<Dashboard> = {
+ resourceLabel: 'Dashboard',
+ nameLabel: 'Dashboard title',
+ descriptionLabel: 'Dashboard description',
+ idProperty: 'elementId',
+ nameProperty: 'name',
+ assetLinkType: 'dashboard',
+ assetLinkCheckboxLabel:
+ 'Add the current dashboard to an existing asset',
+ saveResource: resource =>
+ this.dashboardService.saveDashboard(resource).pipe(
+ tap(savedDashboard => {
+ Object.assign(resource, savedDashboard);
+ Object.assign(this.dashboard, savedDashboard);
+ }),
+ ),
};
- this.dashboard.metadata.lastModifiedEpochMs = Date.now();
- this.dashboardService
- .updateDashboard(this.dashboard)
- .subscribe(result => {
+ const dialogRef = this.dialogService.open(ObjectManageDialogComponent, {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('New dashboard'),
+ width: '50vw',
+ data: {
+ createMode: true,
+ resource: this.dashboard,
+ saveMode: 'immediate',
+ resourceConfig,
+ headerTitle: this.translateService.instant('New dashboard'),
+ },
+ });
+
+ dialogRef.afterClosed().subscribe(refresh => {
+ if (refresh) {
+ this.createMode = false;
+ this.originalDashboard = JSON.parse(
+ JSON.stringify(this.dashboard),
+ );
this.routingService.navigateToDashboardOverview(true);
- });
+ }
+ });
+ }
+
+ manageDashboard(): void {
+ const resource: Dashboard = { ...this.dashboard };
+ const resourceConfig: ObjectManageDialogResourceConfig<Dashboard> = {
+ resourceLabel: 'Dashboard',
+ nameLabel: 'Dashboard title',
+ descriptionLabel: 'Dashboard description',
+ nameProperty: 'name',
+ assetLinkType: 'dashboard',
+ assetLinkCheckboxLabel:
+ 'Add the current dashboard 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.elementId,
+ resource,
+ saveMode: 'deferred',
+ resourceConfig,
+ headerTitle:
+ this.translateService.instant('Manage Dashboard ') +
+ resource.name,
+ },
+ });
+ dialogRef.afterClosed().subscribe(result => {
+ if (result && typeof result !== 'boolean') {
+ this.pendingManageDashboardResult = result;
+ Object.assign(this.dashboard, result.resource);
+ this.breadcrumbService.updateBreadcrumb(
+ this.breadcrumbService.makeRoute(
+ [SpDashboardRoutes.BASE],
+ this.dashboard.name,
+ ),
+ );
+ }
+ });
}
startEditMode(widgetModel: DataExplorerWidgetModel) {
@@ -272,7 +419,7 @@ export class DashboardPanelComponent
}
discardChanges() {
- this.routingService.navigateToDataViewOverview(true);
+ this.routingService.navigateToDashboardOverview(true);
}
triggerEditMode() {
@@ -290,6 +437,45 @@ export class DashboardPanelComponent
});
}
+ private initializeNewDashboard(): void {
+ this.dashboard = {
+ dashboardGeneralSettings: {
+ chartOverrides: {
+ hideToolbox: false,
+ },
+ defaultViewMode: 'grid',
+ globalTimeEnabled: true,
+ gridRowHeightPx: 90,
+ },
+ widgets: [],
+ name: '',
+ dashboardLiveSettings: {
+ refreshModeActive: false,
+ refreshIntervalInSeconds: 10,
+ label: this.translateService.instant('Off'),
+ },
+ dashboardTimeSettings:
+ this.timeSelectionService.getDefaultTimeSettings(),
+ metadata: {
+ createdAtEpochMs: Date.now(),
+ lastModifiedEpochMs: Date.now(),
+ },
+ gridColumns: 12,
+ };
+ this.widgets = [];
+ this.originalDashboard = JSON.parse(JSON.stringify(this.dashboard));
+ this.viewMode = this.dashboard.dashboardGeneralSettings.defaultViewMode;
+ this.timeSettings = this.dashboard.dashboardTimeSettings;
+ this.dashboardLoaded = true;
+ this.breadcrumbService.updateBreadcrumb(
+ this.breadcrumbService.makeRoute(
+ [SpDashboardRoutes.BASE],
+ this.translateService.instant('New dashboard'),
+ ),
+ );
+ this.modifyRefreshInterval(this.dashboard.dashboardLiveSettings);
+ }
+
getDashboard(dashboardId: string, startTime: number, endTime: number) {
this.dashboardService
.getCompositeDashboard(dashboardId)
@@ -310,6 +496,7 @@ export class DashboardPanelComponent
this.dataExplorerDashboardService.makeUniqueWidgetId();
});
this.dashboard = compositeDashboard.dashboard;
+ this.initializeDashboardSettings();
this.widgets = compositeDashboard.widgets;
this.originalDashboard = JSON.parse(
JSON.stringify(compositeDashboard.dashboard),
@@ -324,12 +511,6 @@ export class DashboardPanelComponent
this.viewMode =
this.dashboard.dashboardGeneralSettings.defaultViewMode ||
'grid';
- if (
- this.dashboard.dashboardGeneralSettings
- .globalTimeEnabled === undefined
- ) {
- this.dashboard.dashboardGeneralSettings.globalTimeEnabled = true;
- }
if (!this.dashboard.dashboardTimeSettings.startTime) {
this.dashboard.dashboardTimeSettings =
this.timeSelectionService.getDefaultTimeSettings();
@@ -361,7 +542,7 @@ export class DashboardPanelComponent
}
goBackToOverview() {
- this.routingService.navigateToDataViewOverview();
+ this.routingService.navigateToDashboardOverview();
}
confirmLeaveDialog(
@@ -387,9 +568,9 @@ export class DashboardPanelComponent
if (dialogResult === 'confirm') {
this.dashboard.dashboardGeneralSettings.defaultViewMode =
this.viewMode;
- return this.dashboardService
- .updateDashboard(this.dashboard)
- .pipe(map(() => true));
+ return this.saveDashboardChanges().pipe(
+ map(() => true),
+ );
}
if (dialogResult === 'cancel') {
@@ -404,6 +585,74 @@ export class DashboardPanelComponent
}
}
+ private saveDashboardChanges(): Observable<unknown> {
+ this.dashboard.metadata ??= {
+ createdAtEpochMs: undefined,
+ lastModifiedEpochMs: undefined,
+ };
+ this.dashboard.metadata.lastModifiedEpochMs = Date.now();
+ return this.dashboardService.updateDashboard(this.dashboard).pipe(
+ tap(savedDashboard => {
+ Object.assign(this.dashboard, savedDashboard);
+ }),
+ switchMap(() => from(this.savePendingManageDashboardChanges())),
+ );
+ }
+
+ private async savePendingManageDashboardChanges(): Promise<void> {
+ const result = this.pendingManageDashboardResult;
+ if (!result) {
+ return;
+ }
+
+ if (result.permission) {
+ await firstValueFrom(
+ this.permissionsService.updatePermission(result.permission),
+ );
+ }
+
+ if (this.shouldSaveManageDashboardAssets(result)) {
+ await this.assetSaveService.saveSelectedAssets(
+ result.selectedAssets,
+ this.createDashboardLinkageData(result.resource),
+ result.deselectedAssets,
+ result.originalAssets,
+ );
+ }
+
+ this.pendingManageDashboardResult = undefined;
+ }
+
+ private shouldSaveManageDashboardAssets(
+ result: ObjectManageDialogResult<Dashboard>,
+ ): boolean {
+ return (
+ result.addToAssets &&
+ (result.selectedAssets.length > 0 ||
+ result.deselectedAssets.length > 0 ||
+ result.originalAssets.length > 0)
+ );
+ }
+
+ private createDashboardLinkageData(dashboard: Dashboard): LinkageData[] {
+ return [
+ {
+ type: 'dashboard',
+ id: dashboard.elementId,
+ name: dashboard.name,
+ },
+ ];
+ }
+
+ private initializeDashboardSettings(): void {
+ this.dashboard.dashboardGeneralSettings ??= {};
+ this.dashboard.dashboardGeneralSettings.defaultViewMode ||= 'grid';
+ this.dashboard.dashboardGeneralSettings.globalTimeEnabled ??= true;
+ this.dashboard.dashboardGeneralSettings.chartOverrides ??= {};
+ this.dashboard.dashboardGeneralSettings.chartOverrides.hideToolbox ??= false;
+ this.dashboard.dashboardGeneralSettings.gridRowHeightPx ??= 90;
+ }
+
modifyRefreshInterval(liveSettings: DashboardLiveSettings): void {
this.dashboard.dashboardLiveSettings = liveSettings;
this.refresh$?.unsubscribe();
diff --git a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
index ff335b85ae..bec13db425 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
+++ b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.html
@@ -26,13 +26,13 @@
>
<button
mat-flat-button
- [matTooltip]="'Save' | translate"
+ [matTooltip]="(createMode ? 'Create' : 'Save') | translate"
class="edit-menu-btn"
(click)="saveDashboardEmitter.emit()"
data-cy="save-dashboard-btn"
>
<mat-icon>save</mat-icon>
- <span>{{ 'Save' | translate }}</span>
+ <span>{{ (createMode ? 'Create' : 'Save') | translate }}</span>
</button>
<button
mat-flat-button
@@ -92,14 +92,16 @@
"
>
</sp-time-range-selector>
- <button
- mat-icon-button
- [matMenuTriggerFor]="optMenu"
- [attr.aria-label]="'Options' | translate"
- data-cy="options-data-explorer"
- >
- <mat-icon>more_vert</mat-icon>
- </button>
+ @if (!createMode) {
+ <button
+ mat-icon-button
+ [matMenuTriggerFor]="optMenu"
+ [attr.aria-label]="'Options' | translate"
+ data-cy="options-data-explorer"
+ >
+ <mat-icon>more_vert</mat-icon>
+ </button>
+ }
<mat-menu #optMenu="matMenu">
@if (!editMode && hasDashboardWritePrivileges) {
<button
@@ -123,6 +125,12 @@
<span>{{ 'Hide time range selector' | translate }}</span>
</button>
}
+ @if (editMode && hasDashboardWritePrivileges) {
+ <button mat-menu-item (click)="manageDashboardEmitter.emit()">
+ <mat-icon>settings</mat-icon>
+ <span>{{ 'Manage dashboard' | translate }}</span>
+ </button>
+ }
@if (hasDashboardWritePrivileges) {
<button mat-menu-item (click)="deleteDashboardEmitter.emit()">
<mat-icon>clear</mat-icon>
diff --git a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.ts b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.ts
index b80cf9778e..9cb40700ba 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.ts
+++ b/ui/src/app/dashboard/components/panel/dashboard-toolbar/dashboard-toolbar.component.ts
@@ -71,6 +71,9 @@ export class DashboardToolbarComponent {
@Input()
hasDashboardWritePrivileges: boolean;
+ @Input()
+ createMode: boolean;
+
@Input()
timeSettings: TimeSettings;
@@ -86,6 +89,9 @@ export class DashboardToolbarComponent {
@Output()
deleteDashboardEmitter: EventEmitter<void> = new EventEmitter();
+ @Output()
+ manageDashboardEmitter: EventEmitter<void> = new EventEmitter();
+
@Output()
triggerEditModeEmitter: EventEmitter<void> = new EventEmitter<void>();