This is an automated email from the ASF dual-hosted git repository.
riemer pushed a commit to branch 3810-support-dashboard-clones
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/3810-support-dashboard-clones
by this push:
new 1fbbba8583 feat(#3810): Support cloning of dashboards
1fbbba8583 is described below
commit 1fbbba85833cd0a0336cd204551835cc67e36dff
Author: Dominik Riemer <[email protected]>
AuthorDate: Thu Oct 2 09:45:40 2025 +0200
feat(#3810): Support cloning of dashboards
---
...omponent.scss => sp-table-actions.directive.ts} | 17 +-
.../components/sp-table/sp-table.component.html | 55 +++++-
.../components/sp-table/sp-table.component.scss | 4 +
.../lib/components/sp-table/sp-table.component.ts | 31 ++++
.../shared-ui/src/lib/shared-ui.module.ts | 3 +
.../streampipes/shared-ui/src/public-api.ts | 1 +
.../id-generator/id-generator.service.ts | 4 +
.../template/PipelineInvocationBuilder.ts | 96 ----------
.../slide-view/dashboard-slide-view.component.scss | 5 +-
.../dashboard-overview-table.component.html | 195 +++++++++++++--------
.../dashboard-overview-table.component.ts | 18 ++
.../panel/dashboard-panel.component.scss | 1 -
ui/src/app/dashboard/dashboard.module.ts | 4 +
.../clone-dashboard-dialog.component.html | 160 +++++++++++++++++
.../clone-dashboard-dialog.component.scss} | 13 +-
.../clone-dashboard-dialog.component.ts | 130 ++++++++++++++
.../edit-dashboard-dialog.component.ts | 8 +-
ui/src/scss/sp/_variables.scss | 1 +
ui/src/scss/sp/layout.scss | 8 +
ui/src/scss/sp/sp-theme.scss | 3 +-
20 files changed, 557 insertions(+), 200 deletions(-)
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-actions.directive.ts
similarity index 76%
copy from
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
copy to
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-actions.directive.ts
index d390c087f5..0b7e740d94 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table-actions.directive.ts
@@ -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,15 +16,6 @@
*
*/
-.paginator-container {
- border-top: 1px solid rgba(0, 0, 0, 0.12);
-}
-
-.mat-mdc-row:hover {
- background-color: var(--color-bg-1);
-}
-
-.mat-mdc-no-data-row {
- height: var(--mat-table-row-item-container-height, 52px);
- text-align: center;
-}
+import { Directive } from '@angular/core';
+@Directive({ selector: 'ng-template[spTableActions]', standalone: false })
+export class SpTableActionsDirective {}
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
index c9134d4a7f..5f7ad2cabd 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.html
@@ -20,8 +20,61 @@
<table mat-table class="sp-table" [dataSource]="dataSource">
<ng-content></ng-content>
+ @if (showActionsMenu) {
+ <ng-container matColumnDef="actions">
+ <th
+ fxFlex
+ fxLayoutAlign="center center"
+ mat-header-cell
+ *matHeaderCellDef
+ ></th>
+ <td
+ fxFlex
+ fxLayoutAlign="end center"
+ mat-cell
+ *matCellDef="let element"
+ >
+ <div
+ [matMenuTriggerFor]="menu"
+ #menuTrigger="matMenuTrigger"
+ (mouseenter)="mouseEnter(menuTrigger)"
+ (mouseleave)="mouseLeave(menuTrigger)"
+ >
+ <button
+ mat-icon-button
+ [matMenuTriggerFor]="menu"
+ #menuTrigger="matMenuTrigger"
+ (click)="$event.stopPropagation()"
+ [attr.data-cy]="'more-options'"
+ >
+ <mat-icon>more_vert</mat-icon>
+ </button>
+ </div>
+ <mat-menu #menu="matMenu" [hasBackdrop]="false">
+ <div
+ (mouseenter)="mouseEnter(menuTrigger)"
+ (mouseleave)="mouseLeave(menuTrigger)"
+ >
+ <ng-container
+ *ngTemplateOutlet="
+ actionsTemplate;
+ context: { $implicit: element }
+ "
+ >
+ </ng-container>
+ </div>
+ </mat-menu>
+ </td>
+ </ng-container>
+ }
+
<tr mat-header-row *matHeaderRowDef="columns"></tr>
- <tr mat-row *matRowDef="let row; columns: columns"></tr>
+ <tr
+ mat-row
+ *matRowDef="let row; columns: columns"
+ (click)="rowClicked.emit(row)"
+ [ngClass]="rowsClickable ? 'cursor-pointer' : ''"
+ ></tr>
<tr class="mat-row" *matNoDataRow>
<td
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
index d390c087f5..bd8eea251f 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
@@ -28,3 +28,7 @@
height: var(--mat-table-row-item-container-height, 52px);
text-align: center;
}
+
+.cursor-pointer {
+ cursor: pointer;
+}
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
index 2b31397b83..42fa5d2d4f 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
+++
b/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.ts
@@ -22,8 +22,11 @@ import {
Component,
ContentChild,
ContentChildren,
+ EventEmitter,
Input,
+ Output,
QueryList,
+ TemplateRef,
ViewChild,
} from '@angular/core';
import {
@@ -35,6 +38,8 @@ import {
MatTableDataSource,
} from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
+import { SpTableActionsDirective } from './sp-table-actions.directive';
+import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'sp-table',
@@ -51,12 +56,20 @@ export class SpTableComponent<T> implements AfterViewInit,
AfterContentInit {
@ViewChild(MatTable, { static: true }) table: MatTable<T>;
@Input() columns: string[];
+ @Input() rowsClickable = false;
+ @Input() showActionsMenu = false;
@Input() dataSource: MatTableDataSource<T>;
+ @Output() rowClicked = new EventEmitter<T>();
+
@ViewChild('paginator') paginator: MatPaginator;
+ @ContentChild(SpTableActionsDirective, { read: TemplateRef })
+ actionsTemplate?: TemplateRef<any>;
pageSize = 1;
+ timedOutCloser: any;
+ trigger: MatMenuTrigger | undefined = undefined;
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
@@ -72,4 +85,22 @@ export class SpTableComponent<T> implements AfterViewInit,
AfterContentInit {
);
this.table.setNoDataRow(this.noDataRow);
}
+
+ mouseEnter(trigger) {
+ if (this.timedOutCloser) {
+ clearTimeout(this.timedOutCloser);
+ }
+ if (this.trigger !== undefined) {
+ this.trigger.closeMenu();
+ }
+ trigger.openMenu();
+ this.trigger = trigger;
+ }
+
+ mouseLeave(trigger) {
+ this.timedOutCloser = setTimeout(() => {
+ trigger.closeMenu();
+ this.trigger = undefined;
+ }, 50);
+ }
}
diff --git a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
index 0ead4013e3..0bbe262e61 100644
--- a/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
+++ b/ui/projects/streampipes/shared-ui/src/lib/shared-ui.module.ts
@@ -98,6 +98,7 @@ import { InputSchemaPropertyComponent } from
'./components/input-schema-panel/in
import { MatExpansionModule } from '@angular/material/expansion';
import { SortByRuntimeNamePipe } from './pipes/sort-by-runtime-name.pipe';
import { DragDropModule } from '@angular/cdk/drag-drop';
+import { SpTableActionsDirective } from
'./components/sp-table/sp-table-actions.directive';
@NgModule({
declarations: [
@@ -149,6 +150,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
InputSchemaPanelComponent,
InputSchemaPropertyComponent,
SortByRuntimeNamePipe,
+ SpTableActionsDirective,
],
imports: [
CommonModule,
@@ -216,6 +218,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
PipelineElementComponent,
InputSchemaPanelComponent,
SidebarResizeComponent,
+ SpTableActionsDirective,
],
})
export class SharedUiModule {}
diff --git a/ui/projects/streampipes/shared-ui/src/public-api.ts
b/ui/projects/streampipes/shared-ui/src/public-api.ts
index ae6cdd29c1..f416efe889 100644
--- a/ui/projects/streampipes/shared-ui/src/public-api.ts
+++ b/ui/projects/streampipes/shared-ui/src/public-api.ts
@@ -42,6 +42,7 @@ export * from
'./lib/components/sp-exception-message/exception-details-dialog/ex
export * from
'./lib/components/sp-exception-message/exception-details/exception-details.component';
export * from './lib/components/sp-label/sp-label.component';
export * from './lib/components/sp-table/sp-table.component';
+export * from './lib/components/sp-table/sp-table-actions.directive';
export * from './lib/components/warning-box/warning-box.component';
export * from './lib/components/time-selector/time-selector.model';
export * from './lib/components/time-selector/time-range-selector.component';
diff --git a/ui/src/app/core-services/id-generator/id-generator.service.ts
b/ui/src/app/core-services/id-generator/id-generator.service.ts
index d3d4a0b069..b75a5b7b83 100644
--- a/ui/src/app/core-services/id-generator/id-generator.service.ts
+++ b/ui/src/app/core-services/id-generator/id-generator.service.ts
@@ -46,4 +46,8 @@ export class IdGeneratorService {
public generatePrefixedId(): string {
return this.idPrefix + this.generate(6);
}
+
+ public generateWithPrefix(prefix: string, length: number): string {
+ return prefix + this.generate(length);
+ }
}
diff --git a/ui/src/app/core-services/template/PipelineInvocationBuilder.ts
b/ui/src/app/core-services/template/PipelineInvocationBuilder.ts
deleted file mode 100644
index 9192a735f3..0000000000
--- a/ui/src/app/core-services/template/PipelineInvocationBuilder.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-import {
- FreeTextStaticProperty,
- MappingPropertyUnary,
- PipelineTemplateInvocation,
-} from '@streampipes/platform-services';
-
-export class PipelineInvocationBuilder {
- private pipelineTemplateInvocation: PipelineTemplateInvocation;
-
- constructor(pipelineTemplateInvocation: PipelineTemplateInvocation) {
- this.pipelineTemplateInvocation = pipelineTemplateInvocation;
- }
-
- public static create(
- pipelineTemplateInvocation: PipelineTemplateInvocation,
- ) {
- return new PipelineInvocationBuilder(pipelineTemplateInvocation);
- }
-
- public setTemplateId(id: string) {
- this.pipelineTemplateInvocation.pipelineTemplateId = id;
- return this;
- }
-
- public setName(name: string) {
- this.pipelineTemplateInvocation.kviName = name;
- return this;
- }
-
- public setFreeTextStaticProperty(name: string, value: string) {
- this.pipelineTemplateInvocation.staticProperties.forEach(property => {
- if (
- property instanceof FreeTextStaticProperty &&
- 'jsplumb_domId2' + name === property.internalName
- ) {
- property.value = value;
- }
- });
-
- return this;
- }
-
- public setOneOfStaticProperty(name: string, value: string) {
- this.pipelineTemplateInvocation.staticProperties.forEach(property => {
- if (
- // property instanceof OneOfStaticProperty &&
- property['@class'] ===
-
'org.apache.streampipes.model.staticproperty.OneOfStaticProperty' &&
- 'jsplumb_domId2' + name === property.internalName
- ) {
- // set selected for selected option
- property.options.forEach(option => {
- if (option.name === value) {
- option.selected = true;
- }
- });
- }
- });
-
- return this;
- }
-
- public setMappingPropertyUnary(name: string, value: string) {
- this.pipelineTemplateInvocation.staticProperties.forEach(property => {
- if (
- property instanceof MappingPropertyUnary &&
- 'jsplumb_domId2' + name === property.internalName
- ) {
- property.selectedProperty = value;
- }
- });
-
- return this;
- }
-
- public build() {
- return this.pipelineTemplateInvocation;
- }
-}
diff --git
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.scss
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.scss
index 96db439b27..dd87689641 100644
---
a/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.scss
+++
b/ui/src/app/dashboard-shared/components/chart-view/slide-view/dashboard-slide-view.component.scss
@@ -23,6 +23,7 @@
cursor: pointer;
margin: 5px 10px;
padding: 5px;
+ background: var(--color-bg-0);
}
.viz-preview-selected {
@@ -32,8 +33,8 @@
.selection-box {
overflow-y: auto;
overflow-x: hidden;
- margin-bottom: 5px;
- height: calc(100vh - 147px);
+ height: calc(100vh - 137px);
+ border-right: 1px solid var(--color-bg-3);
max-width: 100%;
}
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 f3117836fe..235edc259f 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
@@ -24,7 +24,10 @@
<sp-table
fxFlex="100"
[columns]="displayedColumns"
+ [showActionsMenu]="true"
+ [rowsClickable]="true"
[dataSource]="dataSource"
+ (rowClicked)="onRowClicked($event)"
>
<ng-container matColumnDef="name">
<th
@@ -98,77 +101,129 @@
</td>
</ng-container>
- <ng-container matColumnDef="actions">
- <th
- fxFlex
- fxLayoutAlign="center center"
- mat-header-cell
- *matHeaderCellDef
- ></th>
- <td
- fxFlex
- fxLayoutAlign="start center"
- mat-cell
- *matCellDef="let element"
+ <ng-template spTableActions let-element>
+ <button
+ mat-menu-item
+ (click)="showDashboard(element); $event.stopPropagation()"
>
- <div fxLayout="row" fxFlex="100" fxLayoutAlign="end
center">
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Show dashboard' | translate"
- (click)="showDashboard(element)"
- >
- <i class="material-icons">visibility</i>
- </button>
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Edit dashboard' | translate"
- *ngIf="hasDataExplorerWritePrivileges"
- [attr.data-cy]="'edit-dashboard-' + element.name"
- (click)="editDashboard(element)"
- >
- <i class="material-icons">edit</i>
- </button>
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Kiosk mode' | translate"
- (click)="openDashboardInKioskMode(element)"
- >
- <i class="material-icons">open_in_new</i>
- </button>
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Dashboard settings' | translate"
- *ngIf="hasDataExplorerWritePrivileges"
- (click)="openEditDashboardDialog(element)"
- >
- <i class="material-icons">settings</i>
- </button>
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Manage permissions' | translate"
- *ngIf="isAdmin"
- (click)="showPermissionsDialog(element)"
- >
- <i class="material-icons">share</i>
- </button>
- <button
- mat-icon-button
- color="accent"
- [matTooltip]="'Delete chart' | translate"
- *ngIf="hasDataExplorerWritePrivileges"
- [attr.data-cy]="'delete-dashboard-' + element.name"
- (click)="openDeleteDashboardDialog(element)"
- >
- <i class="material-icons">delete</i>
- </button>
- </div>
- </td>
- </ng-container>
+ <mat-icon>visibility</mat-icon>
+ <span>{{ 'Show' | translate }}</span>
+ </button>
+ @if (hasDataExplorerWritePrivileges) {
+ <button
+ mat-menu-item
+ [attr.data-cy]="'edit-dashboard-' + element.name"
+ (click)="editDashboard(element)"
+ >
+ <mat-icon>edit</mat-icon>
+ <span>{{ 'Edit' | translate }}</span>
+ </button>
+ <button
+ mat-menu-item
+ [attr.data-cy]="'clone-dashboard-' + element.name"
+ (click)="openCloneDialog(element)"
+ >
+ <mat-icon>flip_to_front</mat-icon>
+ <span>{{ 'Clone' | translate }}</span>
+ </button>
+ }
+ <button
+ mat-menu-item
+ [attr.data-cy]="'kiosk-mode-dashboard-' + element.name"
+ (click)="openDashboardInKioskMode(element)"
+ >
+ <mat-icon>open_in_new</mat-icon>
+ <span>{{ 'Kiosk mode' | translate }}</span>
+ </button>
+ @if (hasDataExplorerWritePrivileges) {
+ <button
+ mat-menu-item
+ [attr.data-cy]="'edit-dashboard-' + element.name"
+ (click)="openEditDashboardDialog(element)"
+ >
+ <mat-icon>settings</mat-icon>
+ <span>{{ 'Settings' | translate }}</span>
+ </button>
+ }
+ <button
+ mat-menu-item
+ [attr.data-cy]="
+ 'manage-dashboard-permissions-' + element.name
+ "
+ (click)="showPermissionsDialog(element)"
+ >
+ <mat-icon>share</mat-icon>
+ <span>{{ 'Manage permissions' | translate }}</span>
+ </button>
+ @if (hasDataExplorerWritePrivileges) {
+ <button
+ mat-menu-item
+ [attr.data-cy]="'delete-dashboard-' + element.name"
+ (click)="openDeleteDashboardDialog(element)"
+ >
+ <mat-icon>delete</mat-icon>
+ <span>{{ 'Delete' | translate }}</span>
+ </button>
+ }
+ <!-- </mat-menu>-->
+ <!-- <div fxLayout="row" fxFlex="100"
fxLayoutAlign="end center">-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Manage
permissions' | translate"-->
+ <!--
(click)="showDashboard(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">visibility</i>-->
+ <!-- </button>-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Edit dashboard'
| translate"-->
+ <!--
*ngIf="hasDataExplorerWritePrivileges"-->
+ <!--
[attr.data-cy]="'edit-dashboard-' + element.name"-->
+ <!--
(click)="editDashboard(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">edit</i>-->
+ <!-- </button>-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Kiosk mode' |
translate"-->
+ <!--
(click)="openDashboardInKioskMode(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">open_in_new</i>-->
+ <!-- </button>-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Dashboard
settings' | translate"-->
+ <!--
*ngIf="hasDataExplorerWritePrivileges"-->
+ <!--
(click)="openEditDashboardDialog(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">settings</i>-->
+ <!-- </button>-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Manage
permissions' | translate"-->
+ <!-- *ngIf="isAdmin"-->
+ <!--
(click)="showPermissionsDialog(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">share</i>-->
+ <!-- </button>-->
+ <!-- <button-->
+ <!-- mat-icon-button-->
+ <!-- color="accent"-->
+ <!-- [matTooltip]="'Delete chart' |
translate"-->
+ <!--
*ngIf="hasDataExplorerWritePrivileges"-->
+ <!--
[attr.data-cy]="'delete-dashboard-' + element.name"-->
+ <!--
(click)="openDeleteDashboardDialog(element)"-->
+ <!-- >-->
+ <!-- <i
class="material-icons">delete</i>-->
+ <!-- </button>-->
+ <!-- </div>-->
+ <!-- </td>-->
+ </ng-template>
</sp-table>
</div>
</div>
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 43053f6068..e776395959 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
@@ -22,6 +22,7 @@ import { Dashboard, DashboardService } from
'@streampipes/platform-services';
import {
ConfirmDialogComponent,
DateFormatService,
+ PanelType,
} from '@streampipes/shared-ui';
import { SpDataExplorerOverviewDirective } from
'../../../../data-explorer/components/overview/data-explorer-overview.directive';
import { MatDialog } from '@angular/material/dialog';
@@ -29,6 +30,8 @@ import { DataExplorerDashboardService } from
'../../../../dashboard-shared/servi
import { DataExplorerSharedService } from
'../../../../data-explorer-shared/services/data-explorer-shared.service';
import { TranslateService } from '@ngx-translate/core';
import { Router } from '@angular/router';
+import { CloneDashboardDialogComponent } from
'../../../dialogs/clone-dashboard/clone-dashboard-dialog.component';
+import { EditDashboardDialogComponent } from
'../../../dialogs/edit-dashboard/edit-dashboard-dialog.component';
@Component({
selector: 'sp-dashboard-overview-table',
@@ -158,4 +161,19 @@ export class DashboardOverviewTableComponent extends
SpDataExplorerOverviewDirec
makeDashboardKioskUrl(dashboardId: string): string {
return
`${window.location.protocol}//${window.location.host}/#/dashboard-kiosk/${dashboardId}`;
}
+
+ openCloneDialog(dashboard: Dashboard): void {
+ this.dialogService.open(CloneDashboardDialogComponent, {
+ panelType: PanelType.SLIDE_IN_PANEL,
+ title: this.translateService.instant('Clone dashboard'),
+ width: '50vw',
+ data: {
+ dashboard: dashboard,
+ },
+ });
+ }
+
+ onRowClicked(dashboard: Dashboard) {
+ this.showDashboard(dashboard);
+ }
}
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 285b384708..ad4770c30a 100644
--- a/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
+++ b/ui/src/app/dashboard/components/panel/dashboard-panel.component.scss
@@ -41,7 +41,6 @@
.designer-panel {
width: 450px;
- border: 1px solid var(--color-tab-border);
}
.edit-menu-btn {
diff --git a/ui/src/app/dashboard/dashboard.module.ts
b/ui/src/app/dashboard/dashboard.module.ts
index e5a299417e..7e480fdb69 100644
--- a/ui/src/app/dashboard/dashboard.module.ts
+++ b/ui/src/app/dashboard/dashboard.module.ts
@@ -69,6 +69,8 @@ import { EditDashboardDialogComponent } from
'./dialogs/edit-dashboard/edit-dash
import { DashboardOverviewTableComponent } from
'./components/overview/dashboard-overview-table/dashboard-overview-table.component';
import { TranslateModule } from '@ngx-translate/core';
import { DashboardSharedModule } from
'../dashboard-shared/dashboard-shared.module';
+import { CloneDashboardDialogComponent } from
'./dialogs/clone-dashboard/clone-dashboard-dialog.component';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
imports: [
@@ -114,6 +116,7 @@ import { DashboardSharedModule } from
'../dashboard-shared/dashboard-shared.modu
DataExplorerSharedModule,
DashboardSharedModule,
TranslateModule.forChild(),
+ MatProgressSpinnerModule,
RouterModule.forChild([
{
path: '',
@@ -145,6 +148,7 @@ import { DashboardSharedModule } from
'../dashboard-shared/dashboard-shared.modu
ChartSelectionComponent,
EditDashboardDialogComponent,
DashboardOverviewTableComponent,
+ CloneDashboardDialogComponent,
],
providers: [],
exports: [],
diff --git
a/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.html
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.html
new file mode 100644
index 0000000000..36be42ddf3
--- /dev/null
+++
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.html
@@ -0,0 +1,160 @@
+<!--
+ ~ 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" fxLayout="column">
+ @if (compositeDashboard) {
+ <div class="sp-dialog-content p-15">
+ <div fxFlex="100">
+ <div
+ fxFlex="100"
+ fxLayout="column"
+ style="margin: 5px; width: 100%"
+ >
+ <!-- Name -->
+ <mat-form-field
+ class="w-100"
+ floatLabel="auto"
+ color="accent"
+ >
+ <mat-label>{{
+ 'New dashboard title' | translate
+ }}</mat-label>
+ <input
+ id="dvname"
+ #dvname="ngModel"
+ required
+ matInput
+ data-cy="clone-data-view-name"
+ [(ngModel)]="form.name"
+ />
+ <mat-error>{{
+ 'Title must not be empty' | translate
+ }}</mat-error>
+ </mat-form-field>
+
+ <!-- Description (optional) -->
+ <mat-form-field class="w-100" color="accent">
+ <mat-label>{{ 'Description' | translate }}</mat-label>
+ <input
+ matInput
+ [(ngModel)]="form.description"
+ data-cy="clone-data-view-description"
+ />
+ </mat-form-field>
+
+ <!-- Deep clone -->
+ <div class="mt-10" fxLayout="column">
+ <label>{{ 'Clone options' | translate }}</label>
+ <mat-checkbox
+ class="mt-5"
+ [(ngModel)]="form.deepClone"
+ data-cy="clone-deep"
+ >{{ 'Deep clone (also clone widgets)' | translate
}}
+ </mat-checkbox>
+ </div>
+
+ <!-- Allow widget edits (only if deepClone) -->
+ <div
+ class="mt-10 mb-10"
+ fxLayout="column"
+ *ngIf="form.deepClone"
+ >
+ <mat-checkbox
+ [(ngModel)]="form.allowWidgetEdits"
+ data-cy="clone-allow-widget-edits"
+ >{{ 'Modify chart configurations' | translate }}
+ </mat-checkbox>
+ </div>
+
+ <!-- Widget edit grid -->
+ <div *ngIf="form.deepClone && form.allowWidgetEdits">
+ <mat-accordion class="mt-10">
+ @for (
+ w of widgetConfigs;
+ track w.current.elementId
+ ) {
+ <mat-expansion-panel
+ class="mat-elevation-z0 border-1"
+ >
+ <mat-expansion-panel-header>
+ <mat-panel-title>
+ {{
+ w.current.baseAppearanceConfig
+ .widgetTitle
+ }}
+ </mat-panel-title>
+ </mat-expansion-panel-header>
+
+ <div
+ fxLayout="column"
+ class="widget-edit p-10"
+ >
+ <mat-form-field
+ class="w-100"
+ color="accent"
+ >
+ <mat-label>{{
+ 'Chart Name' | translate
+ }}</mat-label>
+ <input
+ matInput
+ [(ngModel)]="
+ w.cloned
+ .baseAppearanceConfig
+ .widgetTitle
+ "
+ />
+ </mat-form-field>
+ </div>
+ </mat-expansion-panel>
+ }
+ </mat-accordion>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <mat-divider></mat-divider>
+
+ <div class="sp-dialog-actions actions-align-left" fxLayoutGap="10px">
+ <button
+ [disabled]="dvname.invalid"
+ mat-button
+ mat-flat-button
+ color="accent"
+ data-cy="clone-save"
+ (click)="onSave()"
+ >
+ {{ 'Clone' | translate }}
+ </button>
+ <button
+ mat-button
+ mat-flat-button
+ class="mat-basic mr-10"
+ (click)="onCancel()"
+ >
+ {{ 'Close' | translate }}
+ </button>
+ </div>
+ } @else {
+ <div fxFlex="100" fxLayoutAlign="center center">
+ <mat-spinner [diameter]="20"></mat-spinner>
+ <h5 class="mt-10">Loading</h5>
+ </div>
+ }
+</div>
diff --git
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.scss
similarity index 76%
copy from
ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
copy to
ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.scss
index d390c087f5..0c040d8f26 100644
---
a/ui/projects/streampipes/shared-ui/src/lib/components/sp-table/sp-table.component.scss
+++
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.scss
@@ -16,15 +16,6 @@
*
*/
-.paginator-container {
- border-top: 1px solid rgba(0, 0, 0, 0.12);
-}
-
-.mat-mdc-row:hover {
- background-color: var(--color-bg-1);
-}
-
-.mat-mdc-no-data-row {
- height: var(--mat-table-row-item-container-height, 52px);
- text-align: center;
+.border-1 {
+ border: 1px solid var(--color-bg-2);
}
diff --git
a/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.ts
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.ts
new file mode 100644
index 0000000000..c9fb21bc80
--- /dev/null
+++
b/ui/src/app/dashboard/dialogs/clone-dashboard/clone-dashboard-dialog.component.ts
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { Component, inject, Input, OnInit } from '@angular/core';
+import {
+ ChartService,
+ CompositeDashboard,
+ Dashboard,
+ DashboardService,
+ DataExplorerWidgetModel,
+} from '@streampipes/platform-services';
+import { DialogRef } from '@streampipes/shared-ui';
+import { IdGeneratorService } from
'../../../core-services/id-generator/id-generator.service';
+import { Observable, zip } from 'rxjs';
+import { TranslateService } from '@ngx-translate/core';
+
+export interface WidgetClone {
+ current: DataExplorerWidgetModel;
+ cloned: DataExplorerWidgetModel;
+}
+
+@Component({
+ selector: 'sp-clone-dashboard-dialog-component',
+ templateUrl: './clone-dashboard-dialog.component.html',
+ styleUrls: ['./clone-dashboard-dialog.component.scss'],
+ standalone: false,
+})
+export class CloneDashboardDialogComponent implements OnInit {
+ private dialogRef = inject(DialogRef<CloneDashboardDialogComponent>);
+ private dashboardService = inject(DashboardService);
+ private chartService = inject(ChartService);
+ private idGeneratorService = inject(IdGeneratorService);
+ private translate = inject(TranslateService);
+
+ static readonly DashboardPrefix = 'sp:dashboardmodel:';
+ static readonly ChartPrefix = 'sp:dataexplorerwidgetmodel:';
+
+ @Input()
+ dashboard: Dashboard;
+
+ compositeDashboard: CompositeDashboard;
+ widgetConfigs: WidgetClone[] = [];
+
+ form;
+
+ ngOnInit() {
+ this.dashboardService
+ .getCompositeDashboard(this.dashboard.elementId)
+ .subscribe(res => {
+ this.compositeDashboard = res.body as CompositeDashboard;
+ this.widgetConfigs = this.compositeDashboard.widgets.map(w => {
+ return {
+ current: w,
+ cloned: JSON.parse(JSON.stringify(w)),
+ };
+ });
+ });
+ this.form = {
+ name: `${this.dashboard.name} (${this.translate.instant('Copy')})`,
+ description: this.dashboard.description,
+ deepClone: false,
+ allowWidgetEdits: false,
+ };
+ }
+
+ onCancel(): void {
+ this.dialogRef.close();
+ }
+
+ onSave(): void {
+ let widget$: Observable<any>[] = [];
+ const clonedDashboard: Dashboard = JSON.parse(
+ JSON.stringify(this.dashboard),
+ );
+ const clonedWidgets = this.widgetConfigs.map(wc =>
+ this.form.allowWidgetEdits ? wc.cloned : wc.current,
+ );
+ clonedDashboard.elementId = this.idGeneratorService.generateWithPrefix(
+ CloneDashboardDialogComponent.DashboardPrefix,
+ 6,
+ );
+ clonedDashboard.rev = undefined;
+ clonedDashboard.metadata.createdAtEpochMs = Date.now();
+ clonedDashboard.metadata.lastModifiedEpochMs = Date.now();
+ clonedDashboard.name = this.form.name;
+ clonedDashboard.description = this.form.description;
+ if (this.form.deepClone) {
+ clonedDashboard.widgets.forEach((widget, index) => {
+ const widgetElementId =
+ this.idGeneratorService.generateWithPrefix(
+ CloneDashboardDialogComponent.ChartPrefix,
+ 6,
+ );
+ const clonedWidget = clonedWidgets.find(
+ w => w.elementId === widget.id,
+ );
+ if (clonedWidget !== undefined) {
+ clonedWidgets[index].elementId = widgetElementId;
+ clonedWidgets[index].metadata.createdAtEpochMs =
Date.now();
+ clonedWidgets[index].metadata.lastModifiedEpochMs =
+ Date.now();
+ clonedWidgets[index].rev = undefined;
+ }
+ widget.id = widgetElementId;
+ });
+ widget$ = clonedWidgets.map(w => this.chartService.saveChart(w));
+ }
+ zip([
+ ...widget$,
+ this.dashboardService.saveDashboard(clonedDashboard),
+ ]).subscribe(() => {
+ this.dialogRef.close(true);
+ });
+ }
+}
diff --git
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
index 01d4221d92..112e70cc41 100644
---
a/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
+++
b/ui/src/app/dashboard/dialogs/edit-dashboard/edit-dashboard-dialog.component.ts
@@ -16,7 +16,7 @@
*
*/
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, inject, Input, OnInit } from '@angular/core';
import { Dashboard, DashboardService } from '@streampipes/platform-services';
import { DialogRef } from '@streampipes/shared-ui';
@@ -30,10 +30,8 @@ export class EditDashboardDialogComponent implements OnInit {
@Input() createMode: boolean;
@Input() dashboard: Dashboard;
- constructor(
- private dialogRef: DialogRef<EditDashboardDialogComponent>,
- private dashboardService: DashboardService,
- ) {}
+ private dialogRef = inject(DialogRef<EditDashboardDialogComponent>);
+ private dashboardService = inject(DashboardService);
ngOnInit() {
if (!this.dashboard.dashboardGeneralSettings.defaultViewMode) {
diff --git a/ui/src/scss/sp/_variables.scss b/ui/src/scss/sp/_variables.scss
index 663001ef74..9f77125c08 100644
--- a/ui/src/scss/sp/_variables.scss
+++ b/ui/src/scss/sp/_variables.scss
@@ -80,6 +80,7 @@ $sp-color-error: #b71c1c;
--mat-menu-container-color: var(--color-bg-0);
--mat-select-panel-background-color: var(--color-bg-0);
--mat-sidenav-container-shape: 0;
+ --mat-sidenav-container-divider-color: var(--color-bg-3);
}
.dark-mode {
diff --git a/ui/src/scss/sp/layout.scss b/ui/src/scss/sp/layout.scss
index b4654d7760..6b736888c5 100644
--- a/ui/src/scss/sp/layout.scss
+++ b/ui/src/scss/sp/layout.scss
@@ -60,6 +60,10 @@
margin-top: 0;
}
+.mt-5 {
+ margin-top: 5px;
+}
+
.mt-10 {
margin-top: 10px;
}
@@ -72,6 +76,10 @@
margin-top: 30px;
}
+.mb-5 {
+ margin-bottom: 5px;
+}
+
.mb-10 {
margin-bottom: 10px;
}
diff --git a/ui/src/scss/sp/sp-theme.scss b/ui/src/scss/sp/sp-theme.scss
index 63cc186e97..3a824e35e9 100644
--- a/ui/src/scss/sp/sp-theme.scss
+++ b/ui/src/scss/sp/sp-theme.scss
@@ -56,7 +56,8 @@ html {
surface-container-high: light-dark(#eeeeee, #1c1c1c),
on-surface: light-dark(#1a1a1a, #e6e6e6),
on-surface-variant: light-dark(#5e5e5e, #b5b5b5),
- surface-tint: transparent
+ surface-tint: transparent,
+ background: light-dark(#fafafa, #121212)
)
);