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>&nbsp;{{ 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>();