diff --git a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java index b7857caf7d..e032f1e984 100644 --- a/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java +++ b/streampipes-model-client/src/main/java/org/apache/streampipes/model/client/user/PermissionBuilder.java @@ -19,7 +19,7 @@ public class PermissionBuilder { - private Permission permission; + private final Permission permission; private PermissionBuilder(String objectInstanceId, Class objectInstanceClass, diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardModel.java b/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardModel.java index 6233b3f90c..cf9ee00ea2 100644 --- a/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardModel.java +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/dashboard/DashboardModel.java @@ -46,12 +46,14 @@ public class DashboardModel implements Storable { private Map dashboardTimeSettings; private Map dashboardGeneralSettings; + private Map dashboardLiveSettings; private List widgets; public DashboardModel() { this.dashboardTimeSettings = new HashMap<>(); this.dashboardGeneralSettings = new HashMap<>(); + this.dashboardLiveSettings = new HashMap<>(); } public String getId() { @@ -133,4 +135,12 @@ public void setElementId(String elementId) { public String getCouchDbId() { return this.elementId; } + + public Map getDashboardLiveSettings() { + return dashboardLiveSettings; + } + + public void setDashboardLiveSettings(Map dashboardLiveSettings) { + this.dashboardLiveSettings = dashboardLiveSettings; + } } diff --git a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/DataExplorerWidgetModel.java b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/DataExplorerWidgetModel.java index 2371363acc..02b244d22c 100644 --- a/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/DataExplorerWidgetModel.java +++ b/streampipes-model/src/main/java/org/apache/streampipes/model/datalake/DataExplorerWidgetModel.java @@ -40,6 +40,9 @@ public class DataExplorerWidgetModel extends DashboardEntity { @JsonSerialize(using = CustomMapSerializer.class, as = Map.class) private Map dataConfig; + @JsonSerialize(using = CustomMapSerializer.class, as = Map.class) + private Map timeSettings; + private String pipelineId; private String measureName; @@ -49,6 +52,7 @@ public DataExplorerWidgetModel() { this.baseAppearanceConfig = new HashMap<>(); this.visualizationConfig = new HashMap<>(); this.dataConfig = new HashMap<>(); + this.timeSettings = new HashMap<>(); } public String getWidgetId() { @@ -107,4 +111,12 @@ public void setMeasureName(String measureName) { this.measureName = measureName; } + public Map getTimeSettings() { + return this.timeSettings; + } + + public void setTimeSettings(Map timeSettings) { + this.timeSettings = timeSettings; + } + } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractDashboardResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractCRUDResourceManager.java similarity index 52% rename from streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractDashboardResourceManager.java rename to streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractCRUDResourceManager.java index 3c1a125b7d..15240a708d 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractDashboardResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/AbstractCRUDResourceManager.java @@ -18,48 +18,54 @@ package org.apache.streampipes.resource.management; import org.apache.streampipes.model.client.user.Permission; -import org.apache.streampipes.model.dashboard.DashboardModel; +import org.apache.streampipes.model.shared.api.Storable; import org.apache.streampipes.model.util.ElementIdGenerator; import org.apache.streampipes.storage.api.CRUDStorage; import java.util.List; -public abstract class AbstractDashboardResourceManager - extends AbstractResourceManager> { +public abstract class AbstractCRUDResourceManager + extends AbstractResourceManager> { - public AbstractDashboardResourceManager(CRUDStorage db) { + private final Class elementClass; + + public AbstractCRUDResourceManager(CRUDStorage db, + Class elementClass) { super(db); + this.elementClass = elementClass; } - public List findAll() { + public List findAll() { return db.findAll(); } - public DashboardModel find(String dashboardId) { - return db.getElementById(dashboardId); + public T find(String elementId) { + return db.getElementById(elementId); } - public void delete(String dashboardId) { - db.deleteElementById(dashboardId); - deletePermissions(dashboardId); + public void delete(String elementId) { + db.deleteElementById(elementId); + deletePermissions(elementId); } - public void create(DashboardModel dashboardModel, String principalSid) { - if (dashboardModel.getElementId() == null) { - dashboardModel.setElementId(ElementIdGenerator.makeElementId(DashboardModel.class)); + public T create(T element, + String principalSid) { + if (element.getElementId() == null) { + element.setElementId(ElementIdGenerator.makeElementId(elementClass)); } - db.persist(dashboardModel); - new PermissionResourceManager().createDefault(dashboardModel.getElementId(), DashboardModel.class, principalSid, + db.persist(element); + new PermissionResourceManager().createDefault(element.getElementId(), elementClass, principalSid, false); + return find(element.getElementId()); } - public void update(DashboardModel dashboardModel) { - db.updateElement(dashboardModel); + public void update(T element) { + db.updateElement(element); } - private void deletePermissions(String dashboardId) { + private void deletePermissions(String elementId) { PermissionResourceManager manager = new PermissionResourceManager(); - List permissions = manager.findForObjectId(dashboardId); + List permissions = manager.findForObjectId(elementId); permissions.forEach(manager::delete); } } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DashboardResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DashboardResourceManager.java index 01f3637413..b6db9d83ac 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DashboardResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DashboardResourceManager.java @@ -17,11 +17,12 @@ */ package org.apache.streampipes.resource.management; +import org.apache.streampipes.model.dashboard.DashboardModel; import org.apache.streampipes.storage.management.StorageDispatcher; -public class DashboardResourceManager extends AbstractDashboardResourceManager { +public class DashboardResourceManager extends AbstractCRUDResourceManager { public DashboardResourceManager() { - super(StorageDispatcher.INSTANCE.getNoSqlStore().getDashboardStorage()); + super(StorageDispatcher.INSTANCE.getNoSqlStore().getDashboardStorage(), DashboardModel.class); } } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerResourceManager.java index 1a66b46695..38cca4a759 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerResourceManager.java @@ -17,11 +17,12 @@ */ package org.apache.streampipes.resource.management; +import org.apache.streampipes.model.dashboard.DashboardModel; import org.apache.streampipes.storage.management.StorageDispatcher; -public class DataExplorerResourceManager extends AbstractDashboardResourceManager { +public class DataExplorerResourceManager extends AbstractCRUDResourceManager { public DataExplorerResourceManager() { - super(StorageDispatcher.INSTANCE.getNoSqlStore().getDataExplorerDashboardStorage()); + super(StorageDispatcher.INSTANCE.getNoSqlStore().getDataExplorerDashboardStorage(), DashboardModel.class); } } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java new file mode 100644 index 0000000000..9176b3bf43 --- /dev/null +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/DataExplorerWidgetResourceManager.java @@ -0,0 +1,29 @@ +/* + * 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. + * + */ + +package org.apache.streampipes.resource.management; + +import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; +import org.apache.streampipes.storage.api.CRUDStorage; + +public class DataExplorerWidgetResourceManager extends AbstractCRUDResourceManager { + + public DataExplorerWidgetResourceManager(CRUDStorage db) { + super(db, DataExplorerWidgetModel.class); + } +} diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java index dc263ee381..0d48e15949 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/PermissionResourceManager.java @@ -34,6 +34,10 @@ public List findAll() { return db.findAll(); } + public Permission find(String elementId) { + return db.getElementById(elementId); + } + public List findForObjectId(String objectInstanceId) { return db.getUserPermissionsForObject(objectInstanceId); } diff --git a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java index 37df63dfc7..5f202f4b81 100644 --- a/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java +++ b/streampipes-resource-management/src/main/java/org/apache/streampipes/resource/management/SpResourceManager.java @@ -17,6 +17,9 @@ */ package org.apache.streampipes.resource.management; +import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; +import org.apache.streampipes.storage.api.CRUDStorage; + public class SpResourceManager { public AdapterResourceManager manageAdapters() { @@ -35,6 +38,10 @@ public DataExplorerResourceManager manageDataExplorer() { return new DataExplorerResourceManager(); } + public DataExplorerWidgetResourceManager manageDataExplorerWidget(CRUDStorage db) { + return new DataExplorerWidgetResourceManager(db); + } + public DataProcessorResourceManager manageDataProcessors() { return new DataProcessorResourceManager(); } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/AbstractDashboardResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/AbstractDashboardResource.java index 644b1f6f9d..da77678804 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/AbstractDashboardResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/AbstractDashboardResource.java @@ -19,7 +19,7 @@ package org.apache.streampipes.rest.impl.dashboard; import org.apache.streampipes.model.dashboard.DashboardModel; -import org.apache.streampipes.resource.management.AbstractDashboardResourceManager; +import org.apache.streampipes.resource.management.AbstractCRUDResourceManager; import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; import org.springframework.http.MediaType; @@ -71,7 +71,7 @@ public ResponseEntity createDashboard(@RequestBody DashboardModel dashboar return ok(); } - protected abstract AbstractDashboardResourceManager getResourceManager(); + protected abstract AbstractCRUDResourceManager getResourceManager(); /** * Do not delete these abstract methods below - required by Spring SPEL (see above) diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/Dashboard.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/Dashboard.java index 429b199e41..ac161a72d7 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/Dashboard.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/dashboard/Dashboard.java @@ -19,7 +19,8 @@ package org.apache.streampipes.rest.impl.dashboard; import org.apache.streampipes.model.client.user.Privilege; -import org.apache.streampipes.resource.management.AbstractDashboardResourceManager; +import org.apache.streampipes.model.dashboard.DashboardModel; +import org.apache.streampipes.resource.management.AbstractCRUDResourceManager; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -29,7 +30,7 @@ public class Dashboard extends AbstractDashboardResource { @Override - protected AbstractDashboardResourceManager getResourceManager() { + protected AbstractCRUDResourceManager getResourceManager() { return getSpResourceManager().manageDashboards(); } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeDashboardResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeDashboardResource.java index f33720487e..3339bae6b0 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeDashboardResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeDashboardResource.java @@ -20,7 +20,8 @@ import org.apache.streampipes.model.client.user.Privilege; -import org.apache.streampipes.resource.management.AbstractDashboardResourceManager; +import org.apache.streampipes.model.dashboard.DashboardModel; +import org.apache.streampipes.resource.management.AbstractCRUDResourceManager; import org.apache.streampipes.rest.impl.dashboard.AbstractDashboardResource; import org.springframework.web.bind.annotation.RequestMapping; @@ -31,7 +32,7 @@ public class DataLakeDashboardResource extends AbstractDashboardResource { @Override - protected AbstractDashboardResourceManager getResourceManager() { + protected AbstractCRUDResourceManager getResourceManager() { return getSpResourceManager().manageDataExplorer(); } diff --git a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java index ced1771899..e8743a93a7 100644 --- a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java +++ b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/datalake/DataLakeWidgetResource.java @@ -18,14 +18,17 @@ package org.apache.streampipes.rest.impl.datalake; -import org.apache.streampipes.model.dashboard.DashboardWidgetModel; +import org.apache.streampipes.model.client.user.Privilege; import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; -import org.apache.streampipes.model.util.ElementIdGenerator; -import org.apache.streampipes.rest.core.base.impl.AbstractRestResource; -import org.apache.streampipes.storage.api.CRUDStorage; +import org.apache.streampipes.resource.management.DataExplorerWidgetResourceManager; +import org.apache.streampipes.resource.management.SpResourceManager; +import org.apache.streampipes.rest.core.base.impl.AbstractAuthGuardedRestResource; +import org.apache.streampipes.rest.security.AuthConstants; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -39,31 +42,44 @@ @RestController @RequestMapping("/api/v3/datalake/dashboard/widgets") -public class DataLakeWidgetResource extends AbstractRestResource { +public class DataLakeWidgetResource extends AbstractAuthGuardedRestResource { + + private final DataExplorerWidgetResourceManager resourceManager; + + public DataLakeWidgetResource() { + this.resourceManager = new SpResourceManager().manageDataExplorerWidget( + getNoSqlStorage().getDataExplorerWidgetStorage() + ); + } @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getAllDataExplorerWidgets() { - return ok(getDataExplorerWidgetStorage().findAll()); + @PreAuthorize(AuthConstants.HAS_READ_DATA_EXPLORER_PRIVILEGE) + @PostFilter("hasPermission(filterObject.elementId, 'READ')") + public List getAllDataExplorerWidgets() { + return resourceManager.findAll(); } @GetMapping(path = "/{widgetId}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getDataExplorerWidget(@PathVariable("widgetId") String widgetId) { - return ok(getDataExplorerWidgetStorage().getElementById(widgetId)); + @PreAuthorize("this.hasReadAuthority() and hasPermission(#elementId, 'READ')") + public ResponseEntity getDataExplorerWidget(@PathVariable("widgetId") String elementId) { + return ok(resourceManager.find(elementId)); } @PutMapping( path = "/{widgetId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("this.hasWriteAuthority() and hasPermission(#dataExplorerWidgetModel.elementId, 'WRITE')") public ResponseEntity modifyDataExplorerWidget( @RequestBody DataExplorerWidgetModel dataExplorerWidgetModel) { - getDataExplorerWidgetStorage().updateElement(dataExplorerWidgetModel); - return ok(getDataExplorerWidgetStorage().getElementById(dataExplorerWidgetModel.getElementId())); + resourceManager.update(dataExplorerWidgetModel); + return ok(resourceManager.find(dataExplorerWidgetModel.getElementId())); } @DeleteMapping(path = "/{widgetId}") - public ResponseEntity deleteDataExplorerWidget(@PathVariable("widgetId") String widgetId) { - getDataExplorerWidgetStorage().deleteElementById(widgetId); + @PreAuthorize("this.hasWriteAuthority() and hasPermission(#elementId, 'WRITE')") + public ResponseEntity deleteDataExplorerWidget(@PathVariable("widgetId") String elementId) { + resourceManager.delete(elementId); return ok(); } @@ -71,16 +87,24 @@ public ResponseEntity deleteDataExplorerWidget(@PathVariable("widgetId") S produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE ) + @PreAuthorize("this.hasWriteAuthority() and hasPermission(#dataExplorerWidgetModel.elementId, 'WRITE')") public ResponseEntity createDataExplorerWidget( @RequestBody DataExplorerWidgetModel dataExplorerWidgetModel) { - String elementId = ElementIdGenerator.makeElementId(DashboardWidgetModel.class); - dataExplorerWidgetModel.setElementId(elementId); - getDataExplorerWidgetStorage().persist(dataExplorerWidgetModel); - return ok(getDataExplorerWidgetStorage().getElementById(elementId)); + return ok(resourceManager.create(dataExplorerWidgetModel, getAuthenticatedUserSid())); + } + + /** + * required by Spring expression + */ + public boolean hasReadAuthority() { + return isAdminOrHasAnyAuthority(Privilege.Constants.PRIVILEGE_READ_DATA_EXPLORER_VIEW_VALUE); } - private CRUDStorage getDataExplorerWidgetStorage() { - return getNoSqlStorage().getDataExplorerWidgetStorage(); + /** + * required by Spring expression + */ + public boolean hasWriteAuthority() { + return isAdminOrHasAnyAuthority(Privilege.Constants.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW_VALUE); } } diff --git a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java index 8b21c7575a..a6d7fc5a6a 100644 --- a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java +++ b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/AvailableMigrations.java @@ -27,6 +27,7 @@ import org.apache.streampipes.service.core.migrations.v093.StoreEmailTemplatesMigration; import org.apache.streampipes.service.core.migrations.v095.MergeFilenamesAndRenameDuplicatesMigration; import org.apache.streampipes.service.core.migrations.v970.AddLinkSettingsMigration; +import org.apache.streampipes.service.core.migrations.v970.DataExplorerDataViewMigration; import java.util.Arrays; import java.util.List; @@ -42,7 +43,8 @@ public List getAvailableMigrations() { new AdapterMigration(), new StoreEmailTemplatesMigration(), new MergeFilenamesAndRenameDuplicatesMigration(), - new AddLinkSettingsMigration() + new AddLinkSettingsMigration(), + new DataExplorerDataViewMigration() ); } } diff --git a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v970/DataExplorerDataViewMigration.java b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v970/DataExplorerDataViewMigration.java new file mode 100644 index 0000000000..c078ef980b --- /dev/null +++ b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/migrations/v970/DataExplorerDataViewMigration.java @@ -0,0 +1,124 @@ +/* + * 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. + * + */ + +package org.apache.streampipes.service.core.migrations.v970; + +import org.apache.streampipes.model.client.user.PermissionBuilder; +import org.apache.streampipes.model.dashboard.DashboardModel; +import org.apache.streampipes.model.datalake.DataExplorerWidgetModel; +import org.apache.streampipes.resource.management.PermissionResourceManager; +import org.apache.streampipes.service.core.migrations.Migration; +import org.apache.streampipes.storage.api.CRUDStorage; +import org.apache.streampipes.storage.management.StorageDispatcher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Migrates data explorer dashboards and data views + * Add permission object for each data view + * Add time settings to data views + * Add live settings to dashboards + */ +public class DataExplorerDataViewMigration implements Migration { + + private static final Logger LOG = LoggerFactory.getLogger(DataExplorerDataViewMigration.class); + + private final CRUDStorage dataViewStorage; + private final CRUDStorage dataExplorerDashboardStorage; + private final PermissionResourceManager permissionResourceManager; + + public DataExplorerDataViewMigration() { + var storage = StorageDispatcher.INSTANCE.getNoSqlStore(); + this.dataViewStorage = storage.getDataExplorerWidgetStorage(); + this.dataExplorerDashboardStorage = storage.getDataExplorerDashboardStorage(); + this.permissionResourceManager = new PermissionResourceManager(); + } + + @Override + public boolean shouldExecute() { + return !findDashboardsToMigrate().isEmpty(); + } + + @Override + public void executeMigration() throws IOException { + var allDashboards = findDashboardsToMigrate(); + var defaultLiveSettings = makeDefaultLiveSettings(); + + LOG.info("Migrating {} data explorer dashboards", allDashboards.size()); + + allDashboards + .forEach(dashboard -> { + var timeSettings = dashboard.getDashboardTimeSettings(); + dashboard.getDashboardGeneralSettings().put("globalTimeEnabled", true); + dashboard.setDashboardLiveSettings(defaultLiveSettings); + dashboard.getWidgets().forEach(widget -> { + var dataView = dataViewStorage.getElementById(widget.getId()); + if (dataView != null) { + dataView.setTimeSettings(timeSettings); + dataViewStorage.updateElement(dataView); + addPermissionsObject(dashboard, dataView); + } + }); + dataExplorerDashboardStorage.updateElement(dashboard); + }); + } + + @Override + public String getDescription() { + return "Migrating data explorer data views"; + } + + private List findDashboardsToMigrate() { + return dataExplorerDashboardStorage + .findAll() + .stream() + .filter(dashboard -> !dashboard.getDashboardGeneralSettings().containsKey("globalTimeEnabled")) + .toList(); + } + + private Map makeDefaultLiveSettings() { + var map = new HashMap(); + map.put("refreshModeActive", false); + map.put("refreshIntervalInSeconds", 10); + map.put("label", "Off"); + + return map; + } + + private void addPermissionsObject(DashboardModel dashboard, + DataExplorerWidgetModel dataView) { + var dashboardPermissions = permissionResourceManager.findForObjectId(dashboard.getElementId()); + if (!dashboardPermissions.isEmpty()) { + var dashboardPermission = dashboardPermissions.get(0); + if (permissionResourceManager.find(dataView.getElementId()) == null) { + var dataViewPermission = PermissionBuilder + .create(dataView.getElementId(), DataExplorerWidgetModel.class, dashboardPermission.getOwnerSid()) + .publicElement(dashboardPermission.isPublicElement()) + .build(); + dataViewPermission.setGrantedAuthorities(dashboardPermission.getGrantedAuthorities()); + permissionResourceManager.create(dataViewPermission); + } + } + } +} diff --git a/ui/cypress/support/utils/DataDownloadDialogUtils.ts b/ui/cypress/support/utils/DataDownloadDialogUtils.ts index ba8f1ec9b4..04ce651078 100644 --- a/ui/cypress/support/utils/DataDownloadDialogUtils.ts +++ b/ui/cypress/support/utils/DataDownloadDialogUtils.ts @@ -34,7 +34,7 @@ export class DataDownloadDialogUtils { DataLakeUtils.editDataView(dataViewName); // select download button - cy.dataCy('download-prepared_data-table').click(); + cy.dataCy('data-view-data-download-btn').click(); // download-customInterval, download-all, download-visible cy.dataCy( diff --git a/ui/cypress/support/utils/datalake/DataLakeBtns.ts b/ui/cypress/support/utils/datalake/DataLakeBtns.ts index 5bb7083108..9f456e2f70 100644 --- a/ui/cypress/support/utils/datalake/DataLakeBtns.ts +++ b/ui/cypress/support/utils/datalake/DataLakeBtns.ts @@ -20,4 +20,12 @@ export class DataLakeBtns { public static refreshDataLakeMeasures() { return cy.dataCy('refresh-data-lake-measures'); } + + public static saveDataViewButton() { + return cy.dataCy('save-data-view-btn').click(); + } + + public static editDataViewButton(widgetName: string) { + return cy.dataCy('edit-data-view-' + widgetName).click(); + } } diff --git a/ui/cypress/support/utils/datalake/DataLakeUtils.ts b/ui/cypress/support/utils/datalake/DataLakeUtils.ts index 41fadcf9b0..bc91d6d550 100644 --- a/ui/cypress/support/utils/datalake/DataLakeUtils.ts +++ b/ui/cypress/support/utils/datalake/DataLakeUtils.ts @@ -24,6 +24,7 @@ import { FileManagementUtils } from '../FileManagementUtils'; import { ConnectUtils } from '../connect/ConnectUtils'; import { ConnectBtns } from '../connect/ConnectBtns'; import { AdapterBuilder } from '../../builder/AdapterBuilder'; +import { differenceInMonths } from 'date-fns'; export class DataLakeUtils { public static goToDatalake() { @@ -99,9 +100,12 @@ export class DataLakeUtils { // DataLakeUtils.addNewWidget(); DataLakeUtils.selectDataSet(dataSet); DataLakeUtils.dataConfigSelectAllFields(); + + DataLakeUtils.selectAppearanceConfig(); + DataLakeUtils.selectDataViewName(dataViewName); + DataLakeUtils.openVisualizationConfig(); DataLakeUtils.selectVisualizationType(widgetType); - DataLakeUtils.clickCreateButton(); cy.wait(1000); } @@ -132,29 +136,80 @@ export class DataLakeUtils { PrepareTestDataUtils.loadDataIntoDataLake('fileTest/random.csv'); } - public static createAndEditDataView(name: string) { + public static createAndEditDashboard(name: string) { // Create new data view - cy.dataCy('open-new-data-view-dialog').click(); + cy.dataCy('open-new-dashboard-dialog').click(); // Configure data view cy.dataCy('data-view-name').type(name); cy.dataCy('save-data-view').click(); - this.editDataView(name); + this.editDashboard(name); + } + + public static addDataViewToDashboard(dataViewName: string) { + this.selectTimeRange( + new Date(2020, 10, 20, 22, 44), + this.getFutureDate(), + ); + cy.dataCy('add-data-view-btn-' + dataViewName).click(); + } + + public static createAndEditDataView(name: string) { + // Create new data view + cy.dataCy('open-new-data-view').click(); } - public static removeWidget(widgetName: string) { - cy.dataCy('remove-' + widgetName).click(); + public static removeWidget(dataViewName: string) { + cy.dataCy('remove-' + dataViewName).click(); + } + + public static editDashboard(dashboardName: string) { + // Click edit button + // following only works if single view is available + cy.dataCy('edit-dashboard-' + dashboardName).click(); } public static editDataView(dataViewName: string) { // Click edit button // following only works if single view is available - cy.dataCy('edit-dashboard-' + dataViewName).click(); + cy.dataCy('edit-data-view-' + dataViewName).click(); + } + + public static saveDataViewConfiguration() { + cy.dataCy('save-data-view-btn', { timeout: 10000 }).click(); + } + + public static saveDashboardConfiguration() { + cy.dataCy('save-dashboard-btn', { timeout: 10000 }).click(); + } + + public static deleteDashboard(dashboardName: string) { + cy.dataCy('delete-dashboard-' + dashboardName, { + timeout: 10000, + }).click(); + cy.dataCy('confirm-delete', { timeout: 10000 }).click(); + } + + public static deleteDataView(dataViewName: string) { + cy.dataCy('delete-data-view-' + dataViewName, { + timeout: 10000, + }).click(); + cy.dataCy('confirm-delete', { timeout: 10000 }).click(); + } + + public static cancelDeleteDashboard(dashboardName: string) { + cy.dataCy('delete-dashboard-' + dashboardName, { + timeout: 10000, + }).click(); + cy.dataCy('cancel-delete', { timeout: 10000 }).click(); } - public static saveDataExplorerWidgetConfiguration() { - cy.dataCy('save-data-explorer-widget-btn', { timeout: 10000 }).click(); + public static cancelDeleteDataView(dataViewName: string) { + cy.dataCy('delete-data-view-' + dataViewName, { + timeout: 10000, + }).click(); + cy.dataCy('cancel-delete', { timeout: 10000 }).click(); } public static editWidget(widgetName: string) { @@ -167,12 +222,17 @@ export class DataLakeUtils { } public static saveAndReEditWidget(dataViewName: string) { - // Save configuration - DataLakeUtils.saveDataExplorerWidgetConfiguration(); - DataLakeUtils.goBackToOverview(); + // Save data view configuration + DataLakeUtils.saveDataViewConfiguration(); DataLakeUtils.editDataView(dataViewName); } + public static saveAndReEditDashboard(dashboardName: string) { + // Save dashboard configuration + DataLakeUtils.saveDashboardConfiguration(); + DataLakeUtils.editDashboard(dashboardName); + } + public static clickTab(tabName: string) { // Click start tab to go to overview cy.get('div').contains(tabName).parent().click(); @@ -250,6 +310,14 @@ export class DataLakeUtils { .click(); } + public static clickOrderBy(order: String) { + if (order == 'ascending') { + cy.dataCy('ascending-radio-button').click(); + } else { + cy.dataCy('descending-radio-button').click(); + } + } + /** * Select visualization type */ @@ -277,6 +345,10 @@ export class DataLakeUtils { cy.get('.mdc-tab__text-label').contains('Appearance').parent().click(); } + public static selectDataViewName(dataViewName: string) { + cy.dataCy('appearance-config-widget-title').clear().type(dataViewName); + } + public static clickCreateButton() { // Create widget cy.dataCy('data-explorer-select-data-set-create-btn').click(); @@ -314,8 +386,41 @@ export class DataLakeUtils { } public static selectTimeRange(from: Date, to: Date) { - DataLakeUtils.setTimeInput('time-range-from', from); - DataLakeUtils.setTimeInput('time-range-to', to); + DataLakeUtils.openTimeSelectorMenu(); + const monthsBack = Math.abs(differenceInMonths(from, new Date())) + 1; + DataLakeUtils.navigateCalendar('previous', monthsBack); + DataLakeUtils.selectDay(from.getDate()); + + const monthsForward = Math.abs(differenceInMonths(from, to)); + DataLakeUtils.navigateCalendar('next', monthsForward); + + DataLakeUtils.selectDay(to.getDate()); + + DataLakeUtils.setTimeInput('time-selector-start-time', from); + DataLakeUtils.setTimeInput('time-selector-end-time', to); + DataLakeUtils.applyCustomTimeSelection(); + } + + public static navigateCalendar(direction: string, numberOfMonths: number) { + for (let i = 0; i < numberOfMonths; i++) { + cy.get(`button.mat-calendar-${direction}-button`).click(); + } + } + + public static selectDay(day: number) { + cy.get( + `button:has(span.mat-calendar-body-cell-content:contains("${day}"))`, + ) + .first() + .click(); + } + + public static openTimeSelectorMenu() { + cy.dataCy('time-selector-menu').click(); + } + + public static applyCustomTimeSelection() { + cy.dataCy('apply-custom-time').click(); } public static setTimeInput(field: string, date: Date) { @@ -323,7 +428,7 @@ export class DataLakeUtils { } public static makeTimeString(date: Date) { - return date.toISOString().slice(0, 16); + return date.toTimeString().slice(0, 5); } public static getFutureDate() { @@ -349,4 +454,16 @@ export class DataLakeUtils { .invoke('text') .then(text => text.trim()); } + + public static checkRowsDashboardTable(amount: number) { + cy.dataCy('dashboard-table-overview', { + timeout: 10000, + }).should('have.length', amount); + } + + public static checkRowsViewsTable(amount: number) { + cy.dataCy('data-views-table-overview', { + timeout: 10000, + }).should('have.length', amount); + } } diff --git a/ui/cypress/tests/dataDownloadDialog/dataDownloadDialogTest.smoke.spec.ts b/ui/cypress/tests/dataDownloadDialog/dataDownloadDialogTest.smoke.spec.ts index 574509e074..29f7b79e74 100644 --- a/ui/cypress/tests/dataDownloadDialog/dataDownloadDialogTest.smoke.spec.ts +++ b/ui/cypress/tests/dataDownloadDialog/dataDownloadDialogTest.smoke.spec.ts @@ -21,7 +21,7 @@ import { DataDownloadDialogUtils } from '../../support/utils/DataDownloadDialogU import { DataLakeUtils } from '../../support/utils/datalake/DataLakeUtils'; import { PrepareTestDataUtils } from '../../support/utils/PrepareTestDataUtils'; -describe('Test live data download dialog', () => { +describe('Test data explorer data download dialog', () => { before('Setup Test', () => { cy.initStreamPipesTest(); PrepareTestDataUtils.loadDataIntoDataLake( @@ -30,7 +30,7 @@ describe('Test live data download dialog', () => { ); DataLakeUtils.addDataViewAndTableWidget(dataViewName, 'Persist'); - DataLakeUtils.saveDataExplorerWidgetConfiguration(); + DataLakeUtils.saveDataViewConfiguration(); }); beforeEach('Setup Test', () => { @@ -38,7 +38,7 @@ describe('Test live data download dialog', () => { cy.login(); }); - const dataViewName = 'TestView'; + const dataViewName = 'NewWidget'; const formatTestsExportConfig: ExportConfig = { formatExportConfig: undefined, diff --git a/ui/cypress/tests/datalake/deleteViewAndDashboard.spec.ts b/ui/cypress/tests/datalake/deleteViewAndDashboard.spec.ts new file mode 100644 index 0000000000..742a576813 --- /dev/null +++ b/ui/cypress/tests/datalake/deleteViewAndDashboard.spec.ts @@ -0,0 +1,58 @@ +/* + * 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 { DataLakeUtils } from '../../support/utils/datalake/DataLakeUtils'; + +describe('Test Deletion of Data View and Dashboard', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + DataLakeUtils.loadDataIntoDataLake('datalake/sample.csv', false); + }); + + it('Perform Test', () => { + DataLakeUtils.goToDatalake(); + + DataLakeUtils.addDataViewAndTableWidget('TestView', 'Persist'); + + DataLakeUtils.saveDataViewConfiguration(); + + DataLakeUtils.createAndEditDashboard('TestDashboard'); + + DataLakeUtils.saveDashboardConfiguration(); + + DataLakeUtils.checkRowsDashboardTable(1); + + DataLakeUtils.checkRowsViewsTable(1); + + // Click "Delete" but cancel action and check if dashboard and view are still displayed + DataLakeUtils.cancelDeleteDashboard('TestDashboard'); + + DataLakeUtils.checkRowsDashboardTable(1); + + DataLakeUtils.cancelDeleteDataView('TestView'); + + DataLakeUtils.checkRowsViewsTable(1); + + DataLakeUtils.deleteDashboard('TestDashboard'); + + DataLakeUtils.deleteDataView('TestView'); + + DataLakeUtils.checkRowsDashboardTable(0); + + DataLakeUtils.checkRowsViewsTable(0); + }); +}); diff --git a/ui/cypress/tests/datalake/deleteWidget.ts b/ui/cypress/tests/datalake/deleteWidget.ts index 2de896dcd4..ee5de09d95 100644 --- a/ui/cypress/tests/datalake/deleteWidget.ts +++ b/ui/cypress/tests/datalake/deleteWidget.ts @@ -28,39 +28,33 @@ describe('Test Table View in Data Explorer', () => { * Prepare tests */ DataLakeUtils.addDataViewAndTableWidget('TestView', 'Persist'); + DataLakeUtils.saveDataViewConfiguration(); + DataLakeUtils.createAndEditDashboard('TestDashboard'); + DataLakeUtils.addDataViewToDashboard('TestView'); // Check that widget is visible - cy.dataCy('widget-datalake_configuration', { timeout: 10000 }).should( - 'be.visible', - ); + cy.dataCy('widget-TestView', { timeout: 10000 }).should('be.visible'); // Activate edit mode - DataLakeUtils.saveAndReEditWidget('TestView'); + DataLakeUtils.saveAndReEditDashboard('TestDashboard'); // Delete widget - DataLakeUtils.removeWidget('datalake_configuration'); + DataLakeUtils.removeWidget('TestView'); - // Save dashboard - DataLakeUtils.saveDataExplorerWidgetConfiguration(); + // Go back to dashboard + DataLakeUtils.saveAndReEditDashboard('TestDashboard'); // Check that widget is gone - cy.dataCy('widget-datalake_configuration', { timeout: 10000 }).should( - 'not.exist', - ); + cy.dataCy('widget-TestView', { timeout: 10000 }).should('not.exist'); - // Delete Dashboard - // DataLakeUtils.clickStartTab(); + DataLakeUtils.goBackToOverview(); - // Check that dashboard is gone + DataLakeUtils.checkRowsDashboardTable(1); + + // Delete dashboard + DataLakeUtils.deleteDashboard('TestDashboard'); - cy.dataCy('delete-dashboard-TestView', { timeout: 10000 }).should( - 'have.length', - 1, - ); - cy.dataCy('delete-dashboard-TestView', { timeout: 10000 }).click(); - cy.dataCy('delete-dashboard-TestView', { timeout: 10000 }).should( - 'have.length', - 0, - ); + // Check that dashboard is gone + DataLakeUtils.checkRowsDashboardTable(0); }); }); diff --git a/ui/cypress/tests/datalake/timeOrderDataView.spec.ts b/ui/cypress/tests/datalake/timeOrderDataView.spec.ts new file mode 100644 index 0000000000..75cf2e8db0 --- /dev/null +++ b/ui/cypress/tests/datalake/timeOrderDataView.spec.ts @@ -0,0 +1,74 @@ +/* + * 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 { DataLakeUtils } from '../../support/utils/datalake/DataLakeUtils'; +import { DataLakeBtns } from '../../support/utils/datalake/DataLakeBtns'; + +describe('Test Time Order in Data Explorer', () => { + beforeEach('Setup Test', () => { + cy.initStreamPipesTest(); + DataLakeUtils.loadDataIntoDataLake('datalake/sample.csv', false); + DataLakeUtils.goToDatalake(); + DataLakeUtils.createAndEditDataView('TestView'); + }); + + it('Perform Test with ascending and descending order', () => { + const startDate = new Date(1653871499055); + const endDate = new Date(1653871608093); + + DataLakeUtils.clickOrderBy('descending'); + + DataLakeUtils.openVisualizationConfig(); + DataLakeUtils.selectVisualizationType('Table'); + DataLakeUtils.selectTimeRange(startDate, endDate); + cy.wait(1000); + + cy.dataCy('data-explorer-table').then($cells => { + const strings = $cells.map((index, cell) => cell.innerText).get(); + + // Check for date strings if order is descending + const dateStrings = strings.filter((_, index) => index % 4 === 0); + const dates = dateStrings.map(dateStr => new Date(dateStr)); + const timestamps = dates.map(date => date.getTime()); + for (let i = 0; i < timestamps.length - 1; i++) { + expect(timestamps[i]).to.be.at.least(timestamps[i + 1]); + } + }); + + // Save and leave view, edit view again and check ascending order + DataLakeBtns.saveDataViewButton(); + DataLakeBtns.editDataViewButton('NewWidget'); + DataLakeUtils.clickOrderBy('ascending'); + DataLakeUtils.openVisualizationConfig(); + DataLakeUtils.selectVisualizationType('Table'); + DataLakeUtils.selectTimeRange(startDate, endDate); + cy.wait(1000); + + cy.dataCy('data-explorer-table').then($cells => { + const strings = $cells.map((index, cell) => cell.innerText).get(); + + // Check for date strings if order is ascending + const dateStrings = strings.filter((_, index) => index % 4 === 0); + const dates = dateStrings.map(dateStr => new Date(dateStr)); + const timestamps = dates.map(date => date.getTime()); + for (let i = 0; i < timestamps.length - 1; i++) { + expect(timestamps[i]).to.be.at.most(timestamps[i + 1]); + } + }); + }); +}); diff --git a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts index fd60b73af7..d01dab6918 100644 --- a/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts +++ b/ui/cypress/tests/datalake/timeRangeSelectors.spec.ts @@ -17,20 +17,29 @@ */ import { DataLakeUtils } from '../../support/utils/datalake/DataLakeUtils'; +import { + subDays, + subHours, + subMinutes, + subMonths, + subWeeks, + subYears, +} from 'date-fns'; describe('Test Time Range Selectors in Data Explorer', () => { const periods = [ - { selector: '15_min', offset: 15 }, - { selector: '1_hour', offset: 60 }, - { selector: '1_day', offset: 1440 }, - { selector: '1_week', offset: 10080 }, - { selector: '1_month', offset: 43200 }, - { selector: '1_year', offset: 525600 }, + { selector: 1, start: (now: Date) => subMinutes(now, 15) }, + { selector: 2, start: (now: Date) => subHours(now, 1) }, + { selector: 4, start: (now: Date) => subDays(now, 1) }, + { selector: 6, start: (now: Date) => subWeeks(now, 1) }, + { selector: 8, start: (now: Date) => subMonths(now, 1) }, + { selector: 10, start: (now: Date) => subYears(now, 1) }, ]; - const dateAttribute = 'ng-reflect-date'; - const timeRangeFrom = 'time-range-from'; - const timeRangeTo = 'time-range-to'; + const timeRangeFrom = 'time-selector-start-time'; + const timeRangeTo = 'time-selector-end-time'; + const dateRangeFrom = 'time-selector-start-date'; + const dateRangeTo = 'time-selector-end-date'; before('Setup Tests', () => { cy.initStreamPipesTest(); @@ -42,74 +51,59 @@ describe('Test Time Range Selectors in Data Explorer', () => { it('Perform Test', () => { periods.forEach(period => { cy.log('Testing period: ' + period.selector); + DataLakeUtils.openTimeSelectorMenu(); // Choosing time period and saving initial start and end dates - cy.dataCy(period.selector).click(); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .then(initialEndDateString => { - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .then(initialStartDateString => { - const initialStartDate = parseDate( - initialStartDateString, - ); - const initialEndDate = - parseDate(initialEndDateString); - - cy.wrap(initialStartDate).should( - 'deep.eq', - getExpectedStartDate( - initialEndDate, - period.offset, - ), - ); - // Updating time range to previous one and checking start and end dates - cy.dataCy('decrease-time-button').click({ - force: true, - }); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .then(updatedEndDateString => { - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .then(updatedStartDateString => { - const updatedStartDate = parseDate( - updatedStartDateString, - ); - const updatedEndDate = - parseDate(updatedEndDateString); + cy.dataCy(`time-selector-quick-${period.selector}`).click(); + const expectedEndDate = new Date(); + DataLakeUtils.openTimeSelectorMenu(); + // check if dates can differ from the selected dates + const expectedStartDate = getExpectedStartDate( + expectedEndDate, + period.start, + ); + cy.dataCy(dateRangeFrom).should( + 'have.text', + getLocalizedDateString(expectedStartDate), + ); + cy.dataCy(dateRangeTo).should( + 'have.text', + getLocalizedDateString(expectedEndDate), + ); - cy.wrap(updatedStartDate).should( - 'deep.eq', - getExpectedStartDate( - updatedEndDate, - period.offset, - ), - ); - cy.wrap(updatedEndDate).should( - 'deep.eq', - initialStartDate, - ); - }); - }); - // Updating time range to the next one and comparing start and end dates with initial values - cy.dataCy('increase-time-button').click({ - force: true, - }); - cy.dataCy(timeRangeFrom) - .invoke('attr', dateAttribute) - .should('eq', initialStartDateString); - cy.dataCy(timeRangeTo) - .invoke('attr', dateAttribute) - .should('eq', initialEndDateString); - }); + cy.dataCy(timeRangeFrom) + .invoke('val') + .then(actualTime => { + const expectedDate = + getLocalizedTimeString(expectedStartDate); + expect( + isTimeWithinTolerance( + actualTime as string, + expectedDate, + 5, + ), + ).to.be.true; + }); + cy.dataCy(timeRangeTo) + .invoke('val') + .then(actualTime => { + const expectedDate = + getLocalizedTimeString(expectedEndDate); + expect( + isTimeWithinTolerance( + actualTime as string, + expectedDate, + 5, + ), + ).to.be.true; }); + + DataLakeUtils.applyCustomTimeSelection(); }); }); }); -function getExpectedStartDate(endDate: Date, offset: number): Date { - const startDate = new Date(endDate.getTime() - offset * 60000); +function getExpectedStartDate(endDate: Date, startFn: (Date) => Date): Date { + const startDate = startFn(endDate); startDate.setMinutes( startDate.getMinutes() + getTimezoneDifference(endDate, startDate), ); @@ -120,6 +114,28 @@ function getTimezoneDifference(endDate: Date, startDate: Date): number { return endDate.getTimezoneOffset() - startDate.getTimezoneOffset(); } -function parseDate(dateString: string): Date { - return new Date(Date.parse(dateString)); +function getLocalizedDateString(date: Date) { + return date.toLocaleDateString(); +} + +function getLocalizedTimeString(date: Date) { + return date.toLocaleTimeString().slice(0, 8); +} + +function parseTimeStringToSeconds(timeString: string) { + const [hours, minutes, seconds] = timeString.split(':').map(Number); + return hours * 3600 + minutes * 60 + seconds || 0; +} + +function isTimeWithinTolerance( + actualTimeString: string, + expectedTimeString: string, + toleranceInSeconds: number, +) { + const actualTimeInSeconds = parseTimeStringToSeconds(actualTimeString); + const expectedTimeInSeconds = parseTimeStringToSeconds(expectedTimeString); + return ( + Math.abs(actualTimeInSeconds - expectedTimeInSeconds) <= + toleranceInSeconds + ); } diff --git a/ui/cypress/tests/datalake/widgetDataConfiguration.smoke.spec.ts b/ui/cypress/tests/datalake/widgetDataConfiguration.smoke.spec.ts index 0568f70f5e..040bc3cefc 100644 --- a/ui/cypress/tests/datalake/widgetDataConfiguration.smoke.spec.ts +++ b/ui/cypress/tests/datalake/widgetDataConfiguration.smoke.spec.ts @@ -30,7 +30,7 @@ describe('Test Table View in Data Explorer', () => { /** * Prepare tests */ - DataLakeUtils.addDataViewAndTableWidget('TestView', 'Persist'); + DataLakeUtils.addDataViewAndTableWidget('NewWidget', 'Persist'); // Validate that X lines are available DataLakeWidgetTableUtils.checkRows(10); @@ -72,7 +72,7 @@ describe('Test Table View in Data Explorer', () => { DataLakeUtils.dataConfigAddFilter(filterConfig); DataLakeUtils.checkIfFilterIsSet(1); DataLakeWidgetTableUtils.checkRows(4); - DataLakeUtils.saveAndReEditWidget('TestView'); + DataLakeUtils.saveAndReEditWidget('NewWidget'); DataLakeUtils.checkIfFilterIsSet(1); DataLakeWidgetTableUtils.checkRows(4); DataLakeUtils.dataConfigRemoveFilter(); @@ -90,7 +90,7 @@ describe('Test Table View in Data Explorer', () => { .last({ timeout: 10000 }) .contains('c', { timeout: 10000 }); DataLakeWidgetTableUtils.checkRows(10); - DataLakeUtils.saveAndReEditWidget('TestView'); + DataLakeUtils.saveAndReEditWidget('NewWidget'); cy.dataCy('data-explorer-group-by-randomtext') .find('input') .should('be.checked'); diff --git a/ui/cypress/tests/datalake/widgets/timeSeriesSave.spec.ts b/ui/cypress/tests/datalake/widgets/timeSeriesSave.spec.ts index 464b5f2ee8..760c65fd2c 100644 --- a/ui/cypress/tests/datalake/widgets/timeSeriesSave.spec.ts +++ b/ui/cypress/tests/datalake/widgets/timeSeriesSave.spec.ts @@ -28,34 +28,22 @@ describe('Test if widget configuration is updated correctly', () => { // Create first test data view with one time series widget DataLakeUtils.addDataViewAndTimeSeriesWidget(testView1, dataSet); - DataLakeUtils.saveDataExplorerWidgetConfiguration(); + DataLakeUtils.saveDataViewConfiguration(); + cy.wait(1000); // Create second test data view with one time series widget DataLakeUtils.addDataViewAndTimeSeriesWidget(testView2, dataSet); - DataLakeUtils.saveDataExplorerWidgetConfiguration(); + DataLakeUtils.saveDataViewConfiguration(); }); - // This test case has two different options. The first one selects the edit button of the data explorer on the top right - // and the second one uses the edit button of the widget it('Perform Test', () => { - runTestCase(false); - }); - - it('Perform Test', () => { - runTestCase(true); + runTestCase(); }); }); -const runTestCase = (editOption: boolean) => { +const runTestCase = () => { // Visit settings of widget - const widgetName = 'prepared_data-time-series-chart'; - - if (editOption) { - DataLakeUtils.startEditWidget(widgetName); - } else { - cy.dataCy('options-data-explorer').click(); - cy.dataCy('options-edit-dashboard').click(); - } + DataLakeUtils.editDataView(testView1); // Change first field from line plot to scatter plot DataLakeUtils.openVisualizationConfig(); diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts index a9315adca6..320432ad4a 100644 --- a/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/apis/data-view-data-explorer.service.ts @@ -21,7 +21,6 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { Dashboard } from '../model/dashboard/dashboard.model'; import { Injectable } from '@angular/core'; -import { DatalakeRestService } from './datalake-rest.service'; import { DataExplorerWidgetModel, DataLakeMeasure, @@ -34,30 +33,17 @@ import { SharedDatalakeRestService } from './shared-dashboard.service'; export class DataViewDataExplorerService { constructor( private http: HttpClient, - private dataLakeRestService: DatalakeRestService, private sharedDatalakeRestService: SharedDatalakeRestService, ) {} - getVisualizableData(): Observable { - return this.dataLakeRestService.getAllMeasurementSeries().pipe( - map(data => { - return (data as any[]).map(d => - DataLakeMeasure.fromData(d as DataLakeMeasure), - ); - }), - ); - } - getDataViews(): Observable { return this.sharedDatalakeRestService.getDashboards(this.dashboardUrl); } - getDataView(dataViewId: string): Observable { - return this.http.get(this.dashboardUrl + '/' + dataViewId).pipe( - map(data => { - return data as Dashboard; - }), - ); + getDashboard(dashboardId: string): Observable { + return this.http + .get(`${this.dashboardUrl}/${dashboardId}`) + .pipe(map(data => data as Dashboard)); } updateDashboard(dashboard: Dashboard): Observable { @@ -97,6 +83,12 @@ export class DataViewDataExplorerService { return `${this.baseUrl}/api/v3/datalake/dashboard/widgets`; } + getAllWidgets(): Observable { + return this.http + .get(this.dashboardWidgetUrl) + .pipe(map(res => res as DataExplorerWidgetModel[])); + } + getWidget(widgetId: string): Observable { return this.http.get(this.dashboardWidgetUrl + '/' + widgetId).pipe( map(response => { @@ -120,7 +112,7 @@ export class DataViewDataExplorerService { } deleteWidget(widgetId: string): Observable { - return this.http.delete(this.dashboardWidgetUrl + '/' + widgetId); + return this.http.delete(`${this.dashboardWidgetUrl}/${widgetId}`); } updateWidget(widget: DataExplorerWidgetModel): Observable { diff --git a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts index a2cb61594b..76f552ac3e 100644 --- a/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/apis/datalake-rest.service.ts @@ -207,6 +207,7 @@ export class DatalakeRestService { if (endDate) { queryParams.endDate = endDate; } + if (page) { queryParams.page = page; } diff --git a/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts b/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts index c73bedb49d..8a65f934ed 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/dashboard/dashboard.model.ts @@ -28,6 +28,12 @@ export interface ClientDashboardItem extends GridsterItem { id: string; } +export interface DashboardLiveSettings { + refreshModeActive: boolean; + refreshIntervalInSeconds?: number; + label: string; +} + export interface Dashboard { id?: string; name?: string; @@ -36,6 +42,7 @@ export interface Dashboard { widgets?: ClientDashboardItem[]; dashboardTimeSettings?: TimeSettings; dashboardGeneralSettings?: any; + dashboardLiveSettings: DashboardLiveSettings; elementId?: string; rev?: string; } diff --git a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts index 87151909ca..6c305f6dd4 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/datalake/DateRange.ts @@ -19,7 +19,39 @@ export interface TimeSettings { startTime: number; endTime: number; - dynamicSelection: 15 | 60 | 1440 | 10080 | 43800 | 525600 | -1; + // deprecated + dynamicSelection?: 15 | 60 | 1440 | 10080 | 43800 | 525600 | -1; + timeSelectionId?: TimeSelectionId; +} + +export interface TimeString { + startDate: string; + startTime: string; + endDate: string; + endTime: string; +} + +export enum TimeSelectionId { + CUSTOM, + LAST_15_MINUTES, + LAST_HOUR, + CURRENT_HOUR, + LAST_DAY, + CURRENT_DAY, + LAST_WEEK, + CURRENT_WEEK, + LAST_MONTH, + CURRENT_MONTH, + LAST_YEAR, + CURRENT_YEAR, +} + +export interface QuickTimeSelection { + label: string; + timeSelectionId: TimeSelectionId; + startTime: (now: Date) => Date; + endTime: (now: Date) => Date; + addDividerAfter?: boolean; } export class DateRange { diff --git a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts index 59568c24a9..27adcf3a93 100644 --- a/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts +++ b/ui/projects/streampipes/platform-services/src/lib/model/gen/streampipes-model.ts @@ -20,7 +20,7 @@ /* tslint:disable */ /* eslint-disable */ // @ts-nocheck -// Generated using typescript-generator version 3.2.1263 on 2024-08-15 17:36:05. +// Generated using typescript-generator version 3.2.1263 on 2024-08-19 20:20:18. export class NamedStreamPipesEntity implements Storable { '@class': @@ -882,6 +882,9 @@ export class SchemaTransformationRuleDescription extends TransformationRuleDescr } } +/** + * @deprecated since 0.97.0, for removal + */ export class CreateNestedRuleDescription extends SchemaTransformationRuleDescription { '@class': 'org.apache.streampipes.model.connect.rules.schema.CreateNestedRuleDescription'; 'runtimeKey': string; @@ -999,6 +1002,7 @@ export class DashboardItem { export class DashboardModel implements Storable { couchDbId: string; dashboardGeneralSettings: { [index: string]: any }; + dashboardLiveSettings: { [index: string]: any }; dashboardTimeSettings: { [index: string]: any }; description: string; displayHeader: boolean; @@ -1020,6 +1024,9 @@ export class DashboardModel implements Storable { instance.dashboardGeneralSettings = __getCopyObjectFn( __identity(), )(data.dashboardGeneralSettings); + instance.dashboardLiveSettings = __getCopyObjectFn(__identity())( + data.dashboardLiveSettings, + ); instance.dashboardTimeSettings = __getCopyObjectFn(__identity())( data.dashboardTimeSettings, ); @@ -1096,6 +1103,7 @@ export class DataExplorerWidgetModel extends DashboardEntity { dataConfig: { [index: string]: any }; measureName: string; pipelineId: string; + timeSettings: { [index: string]: any }; visualizationConfig: { [index: string]: any }; widgetId: string; widgetType: string; @@ -1117,6 +1125,9 @@ export class DataExplorerWidgetModel extends DashboardEntity { ); instance.measureName = data.measureName; instance.pipelineId = data.pipelineId; + instance.timeSettings = __getCopyObjectFn(__identity())( + data.timeSettings, + ); instance.visualizationConfig = __getCopyObjectFn(__identity())( data.visualizationConfig, ); diff --git a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts index 2ac32f916b..2c2c28c4f7 100644 --- a/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts +++ b/ui/projects/streampipes/platform-services/src/lib/query/data-view-query-generator.service.ts @@ -51,6 +51,7 @@ export class DataViewQueryGeneratorService { return this.dataLakeRestService.getData( sourceConfig.measureName, dataLakeConfiguration, + true, ); }); } diff --git a/ui/projects/streampipes/shared-ui/src/lib/dialog/confirm-dialog/confirm-dialog.component.html b/ui/projects/streampipes/shared-ui/src/lib/dialog/confirm-dialog/confirm-dialog.component.html index f1e70cc7f4..f526d4fc9d 100644 --- a/ui/projects/streampipes/shared-ui/src/lib/dialog/confirm-dialog/confirm-dialog.component.html +++ b/ui/projects/streampipes/shared-ui/src/lib/dialog/confirm-dialog/confirm-dialog.component.html @@ -29,11 +29,13 @@
{{ data.subtitle }}
mat-button (click)="onCancel()" *ngIf="data.confirmAndCancel" + data-cy="cancel-delete" > {{ data.cancelTitle }} + + +
+ + + + + +
+
+ + + + + + + + + + + + + +
diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.scss b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.scss new file mode 100644 index 0000000000..e9e5de91bb --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.scss @@ -0,0 +1,26 @@ +/*! + * 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. + * + */ + +.data-explorer-options { + padding: 0; +} + +.data-explorer-options-item { + display: inline; + margin-right: 10px; +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts new file mode 100644 index 0000000000..365463f26c --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/dashboard-toolbar.component.ts @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Dashboard, TimeSettings } from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-data-explorer-dashboard-toolbar', + templateUrl: './dashboard-toolbar.component.html', + styleUrls: ['./dashboard-toolbar.component.scss'], +}) +export class DataExplorerDashboardToolbarComponent { + @Input() + dashboard: Dashboard; + + @Input() + editMode: boolean; + + @Input() + viewMode: string; + + @Input() + timeRangeVisible: boolean; + + @Input() + hasDataExplorerWritePrivileges: boolean; + + @Input() + hasDataExplorerDeletePrivileges: boolean; + + @Input() + timeSettings: TimeSettings; + + @Output() + viewModeChange: EventEmitter = new EventEmitter(); + + @Output() + saveDashboardEmitter: EventEmitter = new EventEmitter(); + + @Output() + discardDashboardEmitter: EventEmitter = new EventEmitter(); + + @Output() + deleteDashboardEmitter: EventEmitter = new EventEmitter(); + + @Output() + triggerEditModeEmitter: EventEmitter = new EventEmitter(); + + @Output() + updateDateRangeEmitter: EventEmitter = new EventEmitter(); + + @Output() + intervalSettingsChangedEmitter: EventEmitter = + new EventEmitter(); +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.html new file mode 100644 index 0000000000..1a23450b84 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.html @@ -0,0 +1,39 @@ + + +
+ + + + +
diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.ts new file mode 100644 index 0000000000..9bd3b72d02 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-toolbar/refresh-interval-settings/refresh-interval-settings.component.ts @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + Dashboard, + DashboardLiveSettings, +} from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-data-explorer-refresh-interval-settings-component', + templateUrl: './refresh-interval-settings.component.html', +}) +export class DataExplorerRefreshIntervalSettingsComponent implements OnInit { + @Input() + dashboard: Dashboard; + + @Output() + intervalSettingsChangedEmitter: EventEmitter = + new EventEmitter(); + + availableOptions: DashboardLiveSettings[] = [ + { + label: 'Off', + refreshModeActive: false, + }, + { + label: '1 sec', + refreshModeActive: true, + refreshIntervalInSeconds: 1, + }, + { + label: '2 sec', + refreshModeActive: true, + refreshIntervalInSeconds: 2, + }, + { + label: '5 sec', + refreshModeActive: true, + refreshIntervalInSeconds: 5, + }, + { + label: '10 sec', + refreshModeActive: true, + refreshIntervalInSeconds: 10, + }, + { + label: '30 sec', + refreshModeActive: true, + refreshIntervalInSeconds: 30, + }, + { + label: '1 min', + refreshModeActive: true, + refreshIntervalInSeconds: 60, + }, + { + label: '5 min', + refreshModeActive: true, + refreshIntervalInSeconds: 300, + }, + { + label: '30 min', + refreshModeActive: true, + refreshIntervalInSeconds: 60 * 30, + }, + ]; + + ngOnInit() { + if (!this.dashboard.dashboardLiveSettings?.label) { + this.dashboard.dashboardLiveSettings = this.availableOptions[0]; + } + } + + modifyRefreshInterval(option: DashboardLiveSettings): void { + this.dashboard.dashboardLiveSettings = option; + this.intervalSettingsChangedEmitter.emit(); + } +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.html new file mode 100644 index 0000000000..eea0f0d838 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.html @@ -0,0 +1,44 @@ + + +
+
+
+ Configure Dashboard +
+
+
+ + +
+ + +
+
+ +
Layout
+
+
+
+
diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.scss b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.scss similarity index 99% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.scss rename to ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.scss index 13cbc4aacb..e280dd74d5 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.scss +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.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. diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.ts new file mode 100644 index 0000000000..363a5a7e59 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/dashboard-widget-selection-panel.component.ts @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'sp-data-explorer-dashboard-widget-selection-panel', + templateUrl: './dashboard-widget-selection-panel.component.html', + styleUrls: [ + './dashboard-widget-selection-panel.component.scss', + '../../data-view/data-view-designer-panel/data-explorer-designer-panel.component.scss', + ], +}) +export class DataExplorerDashboardWidgetSelectionPanelComponent { + @Output() + addDataViewEmitter: EventEmitter = new EventEmitter(); +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html new file mode 100644 index 0000000000..baa3ae8b6d --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.html @@ -0,0 +1,50 @@ + + +
+
+
{{ dataView.baseAppearanceConfig.widgetTitle }}
+
+ insert_chart + {{ + widgetTypeLabel + }} +
+
+ folder + {{ + dataView.dataConfig.sourceConfigs[0].measureName + }} +
+
+
+ add +
+
diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.scss b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.scss new file mode 100644 index 0000000000..ba50fbc0cd --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.scss @@ -0,0 +1,32 @@ +/*! + * 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. + * + */ + +.data-view-preview-outer { + border: 1px solid var(--color-bg-2); + padding: 10px; +} + +.data-view-preview-outer:hover { + border-color: var(--color-bg-3); + background: var(--color-bg-1); + cursor: pointer; +} + +.data-view-preview-info { + font-size: small; +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.ts new file mode 100644 index 0000000000..4734957e54 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-preview/data-view-preview.component.ts @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { DataExplorerWidgetModel } from '@streampipes/platform-services'; +import { DataExplorerWidgetRegistry } from '../../../../../registry/data-explorer-widget-registry'; + +@Component({ + selector: 'sp-data-explorer-data-view-preview', + templateUrl: './data-view-preview.component.html', + styleUrls: ['./data-view-preview.component.scss'], +}) +export class DataExplorerDataViewPreviewComponent implements OnInit { + @Input() + dataView: DataExplorerWidgetModel; + + widgetTypeLabel: string; + + @Output() + addDataViewEmitter: EventEmitter = new EventEmitter(); + + constructor(private widgetRegistryService: DataExplorerWidgetRegistry) {} + + ngOnInit() { + this.widgetTypeLabel = this.widgetRegistryService.getWidgetTemplate( + this.dataView.widgetType, + ).label; + } +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.html b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.html new file mode 100644 index 0000000000..3fb1c3cccc --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.html @@ -0,0 +1,27 @@ + + +
+
+ + +
+
diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.scss b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.scss similarity index 96% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.scss rename to ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.scss index 13cbc4aacb..198edb0cf3 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.scss +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.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. @@ -15,3 +15,7 @@ * limitations under the License. * */ + +.m-10 { + margin: 10px; +} diff --git a/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.ts b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.ts new file mode 100644 index 0000000000..e3cb045741 --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/dashboard-widget-selection-panel/data-view-selection/data-view-selection.component.ts @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { + DataExplorerWidgetModel, + DataViewDataExplorerService, +} from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-data-explorer-data-view-selection', + templateUrl: './data-view-selection.component.html', + styleUrls: ['./data-view-selection.component.scss'], +}) +export class DataExplorerDataViewSelectionComponent implements OnInit { + @Output() + addDataViewEmitter: EventEmitter = new EventEmitter(); + + dataViews: DataExplorerWidgetModel[] = []; + + constructor(private dataViewService: DataViewDataExplorerService) {} + + ngOnInit() { + this.dataViewService.getAllWidgets().subscribe(dataViews => { + this.dataViews = dataViews.sort((a, b) => + a.baseAppearanceConfig.widgetTitle.localeCompare( + b.baseAppearanceConfig.widgetTitle, + ), + ); + }); + } +} diff --git a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html new file mode 100644 index 0000000000..d7f084a06e --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.html @@ -0,0 +1,97 @@ + + + +
+ + +
+ +
+ + +
+ + +
+
+ +
+

+ This data view is empty and doesn't contain any widgets. +

+
+ + + + +
+
+
+
diff --git a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.scss b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.scss similarity index 90% rename from ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.scss rename to ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.scss index c1e524bb89..285b384708 100644 --- a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.scss +++ b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.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. @@ -20,15 +20,6 @@ height: 50px; } -.data-explorer-options { - padding: 0px; -} - -.data-explorer-options-item { - display: inline; - margin-right: 10px; -} - .m-20 { margin: 20px; } diff --git a/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts new file mode 100644 index 0000000000..a0ec0b773e --- /dev/null +++ b/ui/src/app/data-explorer/components/dashboard/data-explorer-dashboard-panel.component.ts @@ -0,0 +1,288 @@ +/* + * 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, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Observable, of, Subscription, timer } from 'rxjs'; +import { DataExplorerDashboardGridComponent } from '../widget-view/grid-view/data-explorer-dashboard-grid.component'; +import { + ClientDashboardItem, + Dashboard, + DataExplorerWidgetModel, + DataLakeMeasure, + DataViewDataExplorerService, + TimeSelectionId, + TimeSettings, +} from '@streampipes/platform-services'; +import { TimeSelectionService } from '../../services/time-selection.service'; +import { AuthService } from '../../../services/auth.service'; +import { UserPrivilege } from '../../../_enums/user-privilege.enum'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { DataExplorerDashboardSlideViewComponent } from '../widget-view/slide-view/data-explorer-dashboard-slide-view.component'; +import { + ConfirmDialogComponent, + CurrentUserService, + SpBreadcrumbService, +} from '@streampipes/shared-ui'; +import { MatDialog } from '@angular/material/dialog'; +import { map, switchMap } from 'rxjs/operators'; +import { SpDataExplorerRoutes } from '../../data-explorer.routes'; +import { DataExplorerRoutingService } from '../../services/data-explorer-routing.service'; + +@Component({ + selector: 'sp-data-explorer-dashboard-panel', + templateUrl: './data-explorer-dashboard-panel.component.html', + styleUrls: ['./data-explorer-dashboard-panel.component.scss'], +}) +export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { + dashboardLoaded = false; + dashboard: Dashboard; + + /** + * This is the date range (start, end) to view the data and is set in data-explorer.ts + */ + timeSettings: TimeSettings; + viewMode = 'grid'; + + editMode = false; + timeRangeVisible = true; + + @ViewChild('dashboardGrid') + dashboardGrid: DataExplorerDashboardGridComponent; + + @ViewChild('dashboardSlide') + dashboardSlide: DataExplorerDashboardSlideViewComponent; + + hasDataExplorerWritePrivileges = false; + hasDataExplorerDeletePrivileges = false; + + public items: Dashboard[]; + + dataLakeMeasure: DataLakeMeasure; + authSubscription: Subscription; + refreshSubscription: Subscription; + + constructor( + private dataViewDataExplorerService: DataViewDataExplorerService, + private dialog: MatDialog, + private timeSelectionService: TimeSelectionService, + private authService: AuthService, + private currentUserService: CurrentUserService, + private dashboardService: DataViewDataExplorerService, + private route: ActivatedRoute, + private dataViewService: DataViewDataExplorerService, + private routingService: DataExplorerRoutingService, + private breadcrumbService: SpBreadcrumbService, + ) {} + + public ngOnInit() { + const params = this.route.snapshot.params; + const queryParams = this.route.snapshot.queryParams; + + const startTime = params.startTime; + const endTime = params.endTime; + + this.getDashboard(params.id, startTime, endTime); + + this.authSubscription = this.currentUserService.user$.subscribe(_ => { + this.hasDataExplorerWritePrivileges = this.authService.hasRole( + UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW, + ); + this.hasDataExplorerDeletePrivileges = this.authService.hasRole( + UserPrivilege.PRIVILEGE_DELETE_DATA_EXPLORER_VIEW, + ); + if (queryParams.editMode && this.hasDataExplorerWritePrivileges) { + this.editMode = true; + } + }); + } + + ngOnDestroy() { + this.authSubscription?.unsubscribe(); + this.refreshSubscription?.unsubscribe(); + } + + addDataViewToDashboard(dataViewElementId: string) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const dashboardItem = {} as ClientDashboardItem; + dashboardItem.id = dataViewElementId; + dashboardItem.cols = 3; + dashboardItem.rows = 4; + dashboardItem.x = 0; + dashboardItem.y = 0; + this.dashboard.widgets.push(dashboardItem); + if (this.viewMode === 'grid') { + this.dashboardGrid.loadWidgetConfig(dataViewElementId, true); + } else { + this.dashboardSlide.loadWidgetConfig(dataViewElementId, true); + } + } + + persistDashboardChanges() { + this.dashboard.dashboardGeneralSettings.defaultViewMode = this.viewMode; + this.dataViewDataExplorerService + .updateDashboard(this.dashboard) + .subscribe(result => { + this.routingService.navigateToOverview(true); + }); + } + + startEditMode(widgetModel: DataExplorerWidgetModel) { + this.routingService.navigateToDataView( + true, + widgetModel.elementId, + true, + ); + } + + removeDataViewFromDashboard(widgetIndex: number) { + this.dashboard.widgets.splice(widgetIndex, 1); + } + + updateDateRange(timeSettings: TimeSettings) { + let ts = undefined; + if (this.dashboard.dashboardGeneralSettings.globalTimeEnabled) { + this.timeSettings = timeSettings; + this.dashboard.dashboardTimeSettings = timeSettings; + ts = timeSettings; + } + this.timeSelectionService.notify(ts); + } + + discardChanges() { + this.routingService.navigateToOverview(); + } + + triggerEditMode() { + this.editMode = true; + } + + deleteDashboard() { + this.dashboardService.deleteDashboard(this.dashboard).subscribe(_ => { + this.goBackToOverview(); + }); + } + + getDashboard(dashboardId: string, startTime: number, endTime: number) { + this.dataViewService.getDashboard(dashboardId).subscribe(dashboard => { + this.dashboard = dashboard; + this.breadcrumbService.updateBreadcrumb( + this.breadcrumbService.makeRoute( + [SpDataExplorerRoutes.BASE], + this.dashboard.name, + ), + ); + 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(); + } else { + this.timeSelectionService.updateTimeSettings( + this.dashboard.dashboardTimeSettings, + new Date(), + ); + } + this.timeSettings = + startTime && endTime + ? this.overrideTime(+startTime, +endTime) + : this.dashboard.dashboardTimeSettings; + this.dashboardLoaded = true; + this.modifyRefreshInterval(); + }); + } + + overrideTime(startTime: number, endTime: number): TimeSettings { + return { + startTime, + endTime, + dynamicSelection: -1, + timeSelectionId: TimeSelectionId.CUSTOM, + }; + } + + goBackToOverview() { + this.routingService.navigateToOverview(); + } + + confirmLeaveDashboard( + _route: ActivatedRouteSnapshot, + _state: RouterStateSnapshot, + ): Observable { + if (this.editMode) { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '500px', + data: { + title: 'Save changes?', + subtitle: + 'Update all changes to dashboard widgets or discard current changes.', + cancelTitle: 'Discard changes', + okTitle: 'Update', + confirmAndCancel: true, + }, + }); + return dialogRef.afterClosed().pipe( + map(shouldUpdate => { + if (shouldUpdate) { + this.persistDashboardChanges(); + } + return true; + }), + ); + } else { + return of(true); + } + } + + modifyRefreshInterval(): void { + this.refreshSubscription?.unsubscribe(); + if (this.dashboard.dashboardLiveSettings.refreshModeActive) { + this.createQuerySubscription(); + } + } + + createQuerySubscription() { + this.refreshSubscription = timer( + 0, + this.dashboard.dashboardLiveSettings.refreshIntervalInSeconds * + 1000, + ) + .pipe( + switchMap(() => { + this.timeSelectionService.updateTimeSettings( + this.timeSettings, + new Date(), + ); + this.updateDateRange(this.timeSettings); + return of(null); + }), + ) + .subscribe(); + } +} diff --git a/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html new file mode 100644 index 0000000000..52212aeeba --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.html @@ -0,0 +1,79 @@ + + + +
+ + +
+
+ + +
+ + +
+
+ +
+ + +
+
+
+
+
diff --git a/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.scss b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.scss new file mode 100644 index 0000000000..fa736754ff --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.scss @@ -0,0 +1,54 @@ +/* + * 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. + * + */ + +.fixed-height { + height: 50px; +} + +.data-explorer-options { + padding: 0px; +} + +.data-explorer-options-item { + display: inline; + margin-right: 10px; +} + +.m-20 { + margin: 20px; +} + +.h-100 { + height: 100%; +} + +.dashboard-grid { + display: flex; + flex-direction: column; + flex: 1 1 100%; +} + +.designer-panel-container { + width: 100%; + height: 100%; +} + +.designer-panel { + width: 450px; + border: 1px solid var(--color-tab-border); +} diff --git a/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.ts b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.ts new file mode 100644 index 0000000000..a2880731e1 --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-explorer-data-view.component.ts @@ -0,0 +1,143 @@ +/* + * 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, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { + DataExplorerWidgetModel, + DataLakeMeasure, + DataViewDataExplorerService, + TimeSettings, +} from '@streampipes/platform-services'; +import { ActivatedRoute } from '@angular/router'; +import { TimeSelectionService } from '../../services/time-selection.service'; +import { DataExplorerRoutingService } from '../../services/data-explorer-routing.service'; +import { DataExplorerDashboardService } from '../../services/data-explorer-dashboard.service'; + +@Component({ + selector: 'sp-data-explorer-data-view', + templateUrl: './data-explorer-data-view.component.html', + styleUrls: ['./data-explorer-data-view.component.scss'], +}) +export class DataExplorerDataViewComponent implements OnInit { + dataViewLoaded = false; + timeSettings: TimeSettings; + + editMode = true; + dataView: DataExplorerWidgetModel; + dataLakeMeasure: DataLakeMeasure; + gridsterItemComponent: any; + + @ViewChild('panel', { static: false }) outerPanel: ElementRef; + + constructor( + private dashboardService: DataExplorerDashboardService, + private route: ActivatedRoute, + private routingService: DataExplorerRoutingService, + private dataViewService: DataViewDataExplorerService, + private timeSelectionService: TimeSelectionService, + ) {} + + ngOnInit() { + const dataViewId = this.route.snapshot.params.id; + this.editMode = this.route.snapshot.queryParams.editMode; + + if (dataViewId) { + this.loadDataView(dataViewId); + } else { + this.createWidget(); + this.timeSettings = this.makeDefaultTimeSettings(); + this.afterDataViewLoaded(); + } + } + + loadDataView(dataViewId: string): void { + this.dataViewLoaded = false; + this.dataViewService.getWidget(dataViewId).subscribe(res => { + this.dataView = res; + if (!this.dataView.timeSettings?.startTime) { + this.timeSettings = this.makeDefaultTimeSettings(); + } else { + this.timeSelectionService.updateTimeSettings( + this.dataView.timeSettings as TimeSettings, + new Date(), + ); + this.timeSettings = this.dataView.timeSettings as TimeSettings; + } + this.afterDataViewLoaded(); + }); + } + + afterDataViewLoaded(): void { + this.dataViewLoaded = true; + setTimeout(() => { + const width = this.outerPanel.nativeElement.offsetWidth; + const height = this.outerPanel.nativeElement.offsetHeight; + this.gridsterItemComponent = { width, height }; + this.timeSelectionService.notify(this.timeSettings); + }); + } + + editDataView(): void { + this.routingService.navigateToDataView(true, this.dataView.elementId); + } + + makeDefaultTimeSettings(): TimeSettings { + return this.timeSelectionService.getDefaultTimeSettings(); + } + + createWidget() { + this.dataLakeMeasure = new DataLakeMeasure(); + this.dataView = new DataExplorerWidgetModel(); + this.dataView['@class'] = + 'org.apache.streampipes.model.datalake.DataExplorerWidgetModel'; + this.dataView.baseAppearanceConfig = {}; + this.dataView.baseAppearanceConfig.widgetTitle = 'New Widget'; + this.dataView.dataConfig = {}; + this.dataView.dataConfig.ignoreMissingValues = false; + this.dataView.baseAppearanceConfig.backgroundColor = '#FFFFFF'; + this.dataView.baseAppearanceConfig.textColor = '#3e3e3e'; + this.dataView = { ...this.dataView }; + } + + saveDataView(): void { + this.dataView.timeSettings = this.timeSettings; + const observable = + this.dataView.elementId !== undefined + ? this.dataViewService.updateWidget(this.dataView) + : this.dataViewService.saveWidget(this.dataView); + observable.subscribe(() => { + this.routingService.navigateToOverview(); + }); + } + + discardChanges() { + this.routingService.navigateToOverview(); + } + + updateDateRange(timeSettings: TimeSettings) { + this.timeSettings = timeSettings; + this.timeSelectionService.notify(timeSettings); + } + + downloadDataAsFile() { + this.dashboardService.downloadDataAsFile( + this.timeSettings, + this.dataView, + ); + } +} diff --git a/ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html similarity index 95% rename from ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html index d987bbe97b..3dad5eecf2 100644 --- a/ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.html @@ -20,6 +20,7 @@
diff --git a/ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.scss rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.scss diff --git a/ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts similarity index 90% rename from ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts index 9c6adcaaa8..2787e213b4 100644 --- a/ui/src/app/data-explorer/components/designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/appearance-settings/data-explorer-widget-appearance-settings.component.ts @@ -17,10 +17,10 @@ */ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { WidgetConfigurationService } from '../../../services/widget-configuration.service'; +import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; import { DataExplorerWidgetModel } from '@streampipes/platform-services'; -import { WidgetTypeService } from '../../../services/widget-type.service'; -import { DataExplorerWidgetRegistry } from '../../../registry/data-explorer-widget-registry'; +import { WidgetTypeService } from '../../../../services/widget-type.service'; +import { DataExplorerWidgetRegistry } from '../../../../registry/data-explorer-widget-registry'; import { Subscription } from 'rxjs'; @Component({ @@ -32,7 +32,6 @@ export class DataExplorerWidgetAppearanceSettingsComponent implements OnInit, OnDestroy { @Input() currentlyConfiguredWidget: DataExplorerWidgetModel; - @Input() widgetId: string; presetColors: string[] = [ '#39B54A', @@ -81,7 +80,6 @@ export class DataExplorerWidgetAppearanceSettingsComponent triggerViewUpdate() { this.widgetConfigurationService.notify({ - widgetId: this.widgetId, refreshView: true, refreshData: false, }); diff --git a/ui/src/app/data-explorer/components/designer-panel/data-explorer-designer-panel.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-explorer-designer-panel.component.html similarity index 93% rename from ui/src/app/data-explorer/components/designer-panel/data-explorer-designer-panel.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-explorer-designer-panel.component.html index 6aa928dba7..69c7964b5c 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-explorer-designer-panel.component.html +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-explorer-designer-panel.component.html @@ -32,15 +32,6 @@ }}
-
- -
{ - this.dataSettingsPanel.checkSourceTypes(); - }); - } - } - - closeDesignerPanel() { - this.closeDesignerPanelEmitter.emit(); - } - - resetIndex() { - this.selectedIndex = 0; - this.newWidgetMode = true; - } - @ViewChild('dataSettingsPanel') public set content( dataSettingsPanel: DataExplorerWidgetDataSettingsComponent, diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.html similarity index 98% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.html index 28ead5de4d..e63324da6f 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.html +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.html @@ -226,19 +226,19 @@ + + +
diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.scss rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.scss diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.ts similarity index 97% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.ts index 38eb68730f..c7c1a4bfe7 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/data-explorer-widget-data-settings.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/data-explorer-widget-data-settings.component.ts @@ -32,9 +32,9 @@ import { DataViewDataExplorerService, SourceConfig, } from '@streampipes/platform-services'; -import { Tuple2 } from '../../../../core-model/base/Tuple2'; +import { Tuple2 } from '../../../../../core-model/base/Tuple2'; import { zip } from 'rxjs'; -import { WidgetConfigurationService } from '../../../services/widget-configuration.service'; +import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; import { FieldSelectionPanelComponent } from './field-selection-panel/field-selection-panel.component'; import { GroupSelectionPanelComponent } from './group-selection-panel/group-selection-panel.component'; @@ -225,7 +225,6 @@ export class DataExplorerWidgetDataSettingsComponent implements OnInit { triggerDataRefresh() { this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.html similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.html diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.scss rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.scss diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts similarity index 94% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts index d9d892b947..bde51d0a56 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection-panel/field-selection-panel.component.ts @@ -22,8 +22,8 @@ import { FieldConfig, SourceConfig, } from '@streampipes/platform-services'; -import { DataExplorerFieldProviderService } from '../../../../services/data-explorer-field-provider-service'; -import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; +import { DataExplorerFieldProviderService } from '../../../../../services/data-explorer-field-provider-service'; +import { WidgetConfigurationService } from '../../../../../services/widget-configuration.service'; @Component({ selector: 'sp-field-selection-panel', @@ -34,7 +34,6 @@ export class FieldSelectionPanelComponent implements OnInit { MAX_INITIAL_FIELDS = 3; @Input() sourceConfig: SourceConfig; - @Input() widgetId: string; expandFields = false; @@ -97,7 +96,6 @@ export class FieldSelectionPanelComponent implements OnInit { field => (field.selected = selected), ); this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.html similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.html diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.scss rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.scss diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.ts similarity index 91% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.ts index e7db6f6f40..d09ccd362b 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/field-selection/field-selection.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/field-selection/field-selection.component.ts @@ -22,7 +22,7 @@ import { FieldConfig, SourceConfig, } from '@streampipes/platform-services'; -import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; +import { WidgetConfigurationService } from '../../../../../services/widget-configuration.service'; @Component({ selector: 'sp-field-selection', @@ -32,7 +32,6 @@ import { WidgetConfigurationService } from '../../../../services/widget-configur export class FieldSelectionComponent implements OnInit { @Input() field: FieldConfig; @Input() sourceConfig: SourceConfig; - @Input() widgetId: string; @Output() addFieldEmitter: EventEmitter = new EventEmitter(); @@ -47,7 +46,6 @@ export class FieldSelectionComponent implements OnInit { triggerConfigurationUpdate() { this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.html diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts similarity index 90% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts index 03fe6fe2f3..042d2515e2 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/filter-selection-panel/filter-selection-panel.component.ts @@ -23,17 +23,15 @@ import { SelectedFilter, SourceConfig, } from '@streampipes/platform-services'; -import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; -import { DataExplorerFieldProviderService } from '../../../../services/data-explorer-field-provider-service'; +import { WidgetConfigurationService } from '../../../../../services/widget-configuration.service'; +import { DataExplorerFieldProviderService } from '../../../../../services/data-explorer-field-provider-service'; @Component({ selector: 'sp-filter-selection-panel', templateUrl: './filter-selection-panel.component.html', - styleUrls: ['./filter-selection-panel.component.scss'], }) export class FilterSelectionPanelComponent implements OnInit { @Input() sourceConfig: SourceConfig; - @Input() widgetId: string; tagValues: Map = new Map(); @@ -84,7 +82,6 @@ export class FilterSelectionPanelComponent implements OnInit { }; this.sourceConfig.queryConfig.selectedFilters.push(newFilter); this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); @@ -95,7 +92,6 @@ export class FilterSelectionPanelComponent implements OnInit { sourceConfig.queryConfig.selectedFilters.splice(index, 1); this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); @@ -112,7 +108,6 @@ export class FilterSelectionPanelComponent implements OnInit { if (update) { this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/group-selection-panel/group-selection-panel.component.html similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/group-selection-panel/group-selection-panel.component.html diff --git a/ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts similarity index 88% rename from ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts index 80c20febdc..052c8555e1 100644 --- a/ui/src/app/data-explorer/components/designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/group-selection-panel/group-selection-panel.component.ts @@ -17,8 +17,8 @@ */ import { Component, Input, OnInit } from '@angular/core'; -import { DataExplorerFieldProviderService } from '../../../../services/data-explorer-field-provider-service'; -import { WidgetConfigurationService } from '../../../../services/widget-configuration.service'; +import { DataExplorerFieldProviderService } from '../../../../../services/data-explorer-field-provider-service'; +import { WidgetConfigurationService } from '../../../../../services/widget-configuration.service'; import { EventPropertyUnion, FieldConfig, @@ -28,11 +28,9 @@ import { @Component({ selector: 'sp-group-selection-panel', templateUrl: './group-selection-panel.component.html', - styleUrls: ['./group-selection-panel.component.scss'], }) export class GroupSelectionPanelComponent implements OnInit { @Input() sourceConfig: SourceConfig; - @Input() widgetId: string; constructor( private fieldProvider: DataExplorerFieldProviderService, @@ -75,7 +73,6 @@ export class GroupSelectionPanelComponent implements OnInit { field => (field.selected = selected), ); this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); @@ -92,7 +89,6 @@ export class GroupSelectionPanelComponent implements OnInit { triggerConfigurationUpdate() { this.widgetConfigService.notify({ - widgetId: this.widgetId, refreshData: true, refreshView: true, }); diff --git a/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.html new file mode 100644 index 0000000000..9e47655f85 --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.html @@ -0,0 +1,38 @@ + + + + + Newest (descending) + + Oldest (ascending) + + + diff --git a/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.scss new file mode 100644 index 0000000000..e4a64dc771 --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.scss @@ -0,0 +1,22 @@ +/*! + * 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. + * + */ + +.selection-radio-group { + display: flex; + flex-direction: row; +} diff --git a/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.ts new file mode 100644 index 0000000000..2e9ef62a66 --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/data-settings/order-selection-panel/order-selection-panel.component.ts @@ -0,0 +1,43 @@ +/* + * 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, Input, OnInit } from '@angular/core'; +import { SourceConfig } from '@streampipes/platform-services'; +import { WidgetConfigurationService } from '../../../../../services/widget-configuration.service'; + +@Component({ + selector: 'sp-order-selection-panel', + templateUrl: './order-selection-panel.component.html', + styleUrls: ['./order-selection-panel.component.scss'], +}) +export class OrderSelectionPanelComponent implements OnInit { + @Input() sourceConfig: SourceConfig; + + constructor(private widgetConfigService: WidgetConfigurationService) {} + + ngOnInit(): void { + this.sourceConfig.queryConfig.order ??= 'ASC'; + } + + triggerConfigurationUpdate() { + this.widgetConfigService.notify({ + refreshData: true, + refreshView: true, + }); + } +} diff --git a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.html b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.html similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.html rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.html diff --git a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.scss b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.scss rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.scss diff --git a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts similarity index 90% rename from ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts rename to ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts index 8b8b3b8899..cc5bd31b0b 100644 --- a/ui/src/app/data-explorer/components/designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts +++ b/ui/src/app/data-explorer/components/data-view/data-view-designer-panel/visualisation-settings/data-explorer-visualisation-settings.component.ts @@ -24,10 +24,10 @@ import { SimpleChanges, } from '@angular/core'; import { DataExplorerWidgetModel } from '@streampipes/platform-services'; -import { WidgetTypeService } from '../../../services/widget-type.service'; +import { WidgetTypeService } from '../../../../services/widget-type.service'; import { MatSelectChange } from '@angular/material/select'; -import { IWidget } from '../../../models/dataview-dashboard.model'; -import { DataExplorerWidgetRegistry } from '../../../registry/data-explorer-widget-registry'; +import { IWidget } from '../../../../models/dataview-dashboard.model'; +import { DataExplorerWidgetRegistry } from '../../../../registry/data-explorer-widget-registry'; @Component({ selector: 'sp-explorer-visualisation-settings', diff --git a/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html new file mode 100644 index 0000000000..3f54d2b57b --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.html @@ -0,0 +1,73 @@ + + +
+
+ + + + +
+ + +
+
+
diff --git a/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.ts b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.ts new file mode 100644 index 0000000000..167db1895f --- /dev/null +++ b/ui/src/app/data-explorer/components/data-view/data-view-toolbar/data-explorer-data-view-toolbar.component.ts @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TimeSettings } from '@streampipes/platform-services'; + +@Component({ + selector: 'sp-data-explorer-data-view-toolbar', + templateUrl: './data-explorer-data-view-toolbar.component.html', +}) +export class DataExplorerDataViewToolbarComponent { + @Input() + editMode = true; + + @Input() + timeSettings: TimeSettings; + + timeRangeVisible = true; + + @Output() + saveDataViewEmitter: EventEmitter = new EventEmitter(); + + @Output() + discardDataViewEmitter: EventEmitter = new EventEmitter(); + + @Output() + updateDateRangeEmitter: EventEmitter = new EventEmitter(); + + @Output() + downloadFileEmitter: EventEmitter = new EventEmitter(); +} diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.html b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.html deleted file mode 100644 index 0aadcd6c57..0000000000 --- a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.html +++ /dev/null @@ -1,144 +0,0 @@ - - - -
- -
-
- -
- - - - Data View - - - {{ element.name }}
- {{ element.description }} - -
- - - - -
- - - - - -
- -
-
-
-
-
diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.ts deleted file mode 100644 index 6a5e38a4b5..0000000000 --- a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { MatTableDataSource } from '@angular/material/table'; -import { DataExplorerEditDataViewDialogComponent } from '../../dialogs/edit-dashboard/data-explorer-edit-data-view-dialog.component'; -import { - Dashboard, - DataViewDataExplorerService, -} from '@streampipes/platform-services'; -import { - CurrentUserService, - DialogService, - PanelType, - SpBreadcrumbService, -} from '@streampipes/shared-ui'; -import { ObjectPermissionDialogComponent } from '../../../core-ui/object-permission-dialog/object-permission-dialog.component'; -import { UserRole } from '../../../_enums/user-role.enum'; -import { AuthService } from '../../../services/auth.service'; -import { UserPrivilege } from '../../../_enums/user-privilege.enum'; -import { Router } from '@angular/router'; -import { SpDataExplorerRoutes } from '../../data-explorer.routes'; -import { Subscription } from 'rxjs'; - -@Component({ - selector: 'sp-data-explorer-dashboard-overview', - templateUrl: './data-explorer-dashboard-overview.component.html', - styleUrls: ['./data-explorer-dashboard-overview.component.scss'], -}) -export class DataExplorerDashboardOverviewComponent - implements OnInit, OnDestroy -{ - dataSource = new MatTableDataSource(); - displayedColumns: string[] = []; - dashboards: Dashboard[] = []; - - isAdmin = false; - - hasDataExplorerWritePrivileges = false; - hasDataExplorerDeletePrivileges = false; - - authSubscription: Subscription; - - constructor( - private dataViewService: DataViewDataExplorerService, - private dashboardService: DataViewDataExplorerService, - public dialogService: DialogService, - private authService: AuthService, - private currentUserService: CurrentUserService, - private router: Router, - private breadcrumbService: SpBreadcrumbService, - ) {} - - ngOnInit(): void { - this.breadcrumbService.updateBreadcrumb( - this.breadcrumbService.getRootLink(SpDataExplorerRoutes.BASE), - ); - this.authSubscription = this.currentUserService.user$.subscribe( - user => { - this.hasDataExplorerWritePrivileges = this.authService.hasRole( - UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW, - ); - this.hasDataExplorerDeletePrivileges = this.authService.hasRole( - UserPrivilege.PRIVILEGE_DELETE_DATA_EXPLORER_VIEW, - ); - this.isAdmin = user.roles.indexOf(UserRole.ROLE_ADMIN) > -1; - this.displayedColumns = ['name', 'actions']; - }, - ); - - this.getDashboards(); - } - - ngOnDestroy() { - if (this.authSubscription) { - this.authSubscription.unsubscribe(); - } - } - - getDashboards() { - this.dataViewService.getDataViews().subscribe(data => { - this.dashboards = data.sort((a, b) => a.name.localeCompare(b.name)); - this.dataSource.data = this.dashboards; - }); - } - - openNewDataViewDialog() { - const dataViewDashboard: Dashboard = {}; - dataViewDashboard.dashboardGeneralSettings = {}; - dataViewDashboard.widgets = []; - dataViewDashboard.name = ''; - - this.openDataViewModificationDialog(true, dataViewDashboard); - } - - openDataViewModificationDialog(createMode: boolean, dashboard: Dashboard) { - const dialogRef = this.dialogService.open( - DataExplorerEditDataViewDialogComponent, - { - panelType: PanelType.STANDARD_PANEL, - title: createMode ? 'New Data View' : 'Edit Data View', - width: '70vw', - data: { - createMode: createMode, - dashboard: dashboard, - }, - }, - ); - - dialogRef.afterClosed().subscribe(result => { - this.getDashboards(); - }); - } - - showPermissionsDialog(dashboard: Dashboard) { - const dialogRef = this.dialogService.open( - ObjectPermissionDialogComponent, - { - panelType: PanelType.SLIDE_IN_PANEL, - title: 'Manage permissions', - width: '50vw', - data: { - objectInstanceId: dashboard.elementId, - headerTitle: - 'Manage permissions for dashboard ' + dashboard.name, - }, - }, - ); - - dialogRef.afterClosed().subscribe(refresh => { - if (refresh) { - this.getDashboards(); - } - }); - } - - openEditDataViewDialog(dashboard: Dashboard) { - this.openDataViewModificationDialog(false, dashboard); - } - - openDeleteDashboardDialog(dashboard: Dashboard) { - this.dashboardService.deleteDashboard(dashboard).subscribe(() => { - this.getDashboards(); - }); - } - - showDashboard(dashboard: Dashboard) { - this.router.navigate(['dataexplorer/', dashboard.elementId]); - } - - editDashboard(dashboard: Dashboard) { - this.router.navigate(['dataexplorer/', dashboard.elementId], { - queryParams: { action: 'edit' }, - }); - } -} diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html new file mode 100644 index 0000000000..7913b3321b --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.html @@ -0,0 +1,115 @@ + + +
+ +
+ + + + Dashboard + + + {{ element.name }}
+ {{ element.description }} + +
+ + + + +
+ + + + + +
+ +
+
+
+
diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts new file mode 100644 index 0000000000..11dbce196c --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview/data-explorer-dashboard-overview.component.ts @@ -0,0 +1,127 @@ +/* + * 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 } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { + Dashboard, + DataViewDataExplorerService, +} from '@streampipes/platform-services'; +import { ObjectPermissionDialogComponent } from '../../../../core-ui/object-permission-dialog/object-permission-dialog.component'; +import { + CurrentUserService, + ConfirmDialogComponent, + DialogService, +} from '@streampipes/shared-ui'; +import { AuthService } from '../../../../services/auth.service'; +import { SpDataExplorerOverviewDirective } from '../data-explorer-overview.directive'; +import { DataExplorerDashboardService } from '../../../services/data-explorer-dashboard.service'; +import { DataExplorerRoutingService } from '../../../services/data-explorer-routing.service'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'sp-data-explorer-dashboard-overview', + templateUrl: './data-explorer-dashboard-overview.component.html', + styleUrls: ['../data-explorer-overview.component.scss'], +}) +export class SpDataExplorerDashboardOverviewComponent extends SpDataExplorerOverviewDirective { + dataSource = new MatTableDataSource(); + displayedColumns: string[] = []; + dashboards: Dashboard[] = []; + + constructor( + private dataViewService: DataViewDataExplorerService, + private dashboardService: DataViewDataExplorerService, + private dataExplorerDashboardService: DataExplorerDashboardService, + public dialogService: DialogService, + routingService: DataExplorerRoutingService, + authService: AuthService, + currentUserService: CurrentUserService, + private dialog: MatDialog, + ) { + super(dialogService, authService, currentUserService, routingService); + } + + afterInit(): void { + this.displayedColumns = ['name', 'actions']; + this.getDashboards(); + } + + showPermissionsDialog(dashboard: Dashboard) { + const dialogRef = + this.dataExplorerDashboardService.openPermissionsDialog( + dashboard.elementId, + `Manage permissions for dashboard ${dashboard.name}`, + ); + + dialogRef.afterClosed().subscribe(refresh => { + if (refresh) { + this.getDashboards(); + } + }); + } + + openEditDashboardDialog(dashboard: Dashboard) { + const dialogRef = + this.dataExplorerDashboardService.openDashboardModificationDialog( + false, + dashboard, + ); + + dialogRef.afterClosed().subscribe(() => { + this.getDashboards(); + }); + } + + openDeleteDashboardDialog(dashboard: Dashboard) { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '600px', + data: { + title: 'Are you sure you want to delete this dashboard?', + subtitle: 'This action cannot be reversed!', + cancelTitle: 'Cancel', + okTitle: 'Delete Dashboard', + confirmAndCancel: true, + }, + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.dashboardService + .deleteDashboard(dashboard) + .subscribe(() => { + this.getDashboards(); + }); + } + }); + } + + showDashboard(dashboard: Dashboard) { + this.routingService.navigateToDashboard(false, dashboard.elementId); + } + + editDashboard(dashboard: Dashboard) { + this.routingService.navigateToDashboard(true, dashboard.elementId); + } + + getDashboards() { + this.dataViewService.getDataViews().subscribe(data => { + this.dashboards = data.sort((a, b) => a.name.localeCompare(b.name)); + this.dataSource.data = this.dashboards; + }); + } +} diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.html b/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.html new file mode 100644 index 0000000000..65dfd2af0c --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.html @@ -0,0 +1,114 @@ + + +
+ +
+ + + + Data View + + + {{ element.baseAppearanceConfig.widgetTitle }}
+ +
+ + + + +
+ + + + +
+ +
+
+
+
diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.ts new file mode 100644 index 0000000000..41908f3519 --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-data-view-overview/data-explorer-data-view-overview.component.ts @@ -0,0 +1,114 @@ +/* + * 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 } from '@angular/core'; +import { SpDataExplorerOverviewDirective } from '../data-explorer-overview.directive'; +import { MatTableDataSource } from '@angular/material/table'; +import { + Dashboard, + DataExplorerWidgetModel, + DataViewDataExplorerService, +} from '@streampipes/platform-services'; +import { + CurrentUserService, + DialogService, + ConfirmDialogComponent, +} from '@streampipes/shared-ui'; +import { AuthService } from '../../../../services/auth.service'; +import { DataExplorerRoutingService } from '../../../services/data-explorer-routing.service'; +import { DataExplorerDashboardService } from '../../../services/data-explorer-dashboard.service'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'sp-data-explorer-data-view-overview', + templateUrl: './data-explorer-data-view-overview.component.html', + styleUrls: ['../data-explorer-overview.component.scss'], +}) +export class SpDataExplorerDataViewOverviewComponent extends SpDataExplorerOverviewDirective { + dataSource = new MatTableDataSource(); + displayedColumns: string[] = []; + dashboards: Dashboard[] = []; + + constructor( + private dataViewService: DataViewDataExplorerService, + private dataExplorerDashboardService: DataExplorerDashboardService, + public dialogService: DialogService, + authService: AuthService, + currentUserService: CurrentUserService, + routingService: DataExplorerRoutingService, + private dialog: MatDialog, + ) { + super(dialogService, authService, currentUserService, routingService); + } + + afterInit(): void { + this.displayedColumns = ['name', 'actions']; + this.getDataViews(); + } + + getDataViews(): void { + this.dataViewService.getAllWidgets().subscribe(widgets => { + widgets = widgets.sort((a, b) => + a.baseAppearanceConfig.widgetTitle.localeCompare( + b.baseAppearanceConfig.widgetTitle, + ), + ); + this.dataSource.data = widgets; + }); + } + + openDataView(dataView: DataExplorerWidgetModel, editMode: boolean): void { + this.routingService.navigateToDataView(editMode, dataView.elementId); + } + + showPermissionsDialog(dashboard: Dashboard) { + const dialogRef = + this.dataExplorerDashboardService.openPermissionsDialog( + dashboard.elementId, + `Manage permissions for dashboard ${dashboard.name}`, + ); + + dialogRef.afterClosed().subscribe(refresh => { + if (refresh) { + this.getDataViews(); + } + }); + } + + deleteDataView(dataView: DataExplorerWidgetModel) { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '600px', + data: { + title: 'Are you sure you want to delete this data view?', + subtitle: 'This action cannot be reversed!', + cancelTitle: 'Cancel', + okTitle: 'Delete Data View', + confirmAndCancel: true, + }, + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.dataViewService + .deleteWidget(dataView.elementId) + .subscribe(() => { + this.getDataViews(); + }); + } + }); + } +} diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.html b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.html new file mode 100644 index 0000000000..81b53324b8 --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.html @@ -0,0 +1,56 @@ + + + +
+ + +
+
+ + +
+
diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.scss b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.scss similarity index 100% rename from ui/src/app/data-explorer/components/overview/data-explorer-dashboard-overview.component.scss rename to ui/src/app/data-explorer/components/overview/data-explorer-overview.component.scss diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts new file mode 100644 index 0000000000..176399a5fb --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-overview.component.ts @@ -0,0 +1,89 @@ +/* + * 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, ViewChild } from '@angular/core'; +import { Dashboard } from '@streampipes/platform-services'; +import { + CurrentUserService, + DialogService, + SpBreadcrumbService, +} from '@streampipes/shared-ui'; +import { AuthService } from '../../../services/auth.service'; +import { SpDataExplorerRoutes } from '../../data-explorer.routes'; +import { DataExplorerDashboardService } from '../../services/data-explorer-dashboard.service'; +import { SpDataExplorerDashboardOverviewComponent } from './data-explorer-dashboard-overview/data-explorer-dashboard-overview.component'; +import { SpDataExplorerOverviewDirective } from './data-explorer-overview.directive'; +import { DataExplorerRoutingService } from '../../services/data-explorer-routing.service'; + +@Component({ + selector: 'sp-data-explorer-overview', + templateUrl: './data-explorer-overview.component.html', + styleUrls: ['./data-explorer-overview.component.scss'], +}) +export class DataExplorerOverviewComponent extends SpDataExplorerOverviewDirective { + @ViewChild(SpDataExplorerDashboardOverviewComponent) + dashboardOverview: SpDataExplorerDashboardOverviewComponent; + + constructor( + public dialogService: DialogService, + private breadcrumbService: SpBreadcrumbService, + private dataExplorerDashboardService: DataExplorerDashboardService, + authService: AuthService, + currentUserService: CurrentUserService, + routingService: DataExplorerRoutingService, + ) { + super(dialogService, authService, currentUserService, routingService); + } + + afterInit(): void { + this.breadcrumbService.updateBreadcrumb( + this.breadcrumbService.getRootLink(SpDataExplorerRoutes.BASE), + ); + } + + openNewDashboardDialog() { + const dataViewDashboard: Dashboard = { + dashboardGeneralSettings: {}, + widgets: [], + name: '', + dashboardLiveSettings: { + refreshModeActive: false, + refreshIntervalInSeconds: 10, + label: 'Off', + }, + }; + + this.openDashboardModificationDialog(true, dataViewDashboard); + } + + createNewDataView(): void { + this.routingService.navigateToDataView(true); + } + + openDashboardModificationDialog(createMode: boolean, dashboard: Dashboard) { + const dialogRef = + this.dataExplorerDashboardService.openDashboardModificationDialog( + createMode, + dashboard, + ); + + dialogRef.afterClosed().subscribe(() => { + this.dashboardOverview.getDashboards(); + }); + } +} diff --git a/ui/src/app/data-explorer/components/overview/data-explorer-overview.directive.ts b/ui/src/app/data-explorer/components/overview/data-explorer-overview.directive.ts new file mode 100644 index 0000000000..527d43a753 --- /dev/null +++ b/ui/src/app/data-explorer/components/overview/data-explorer-overview.directive.ts @@ -0,0 +1,65 @@ +/* + * 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 { Directive, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { UserPrivilege } from '../../../_enums/user-privilege.enum'; +import { UserRole } from '../../../_enums/user-role.enum'; +import { CurrentUserService, DialogService } from '@streampipes/shared-ui'; +import { AuthService } from '../../../services/auth.service'; +import { DataExplorerRoutingService } from '../../services/data-explorer-routing.service'; + +@Directive() +export abstract class SpDataExplorerOverviewDirective + implements OnInit, OnDestroy +{ + isAdmin = false; + + public hasDataExplorerWritePrivileges = false; + public hasDataExplorerDeletePrivileges = false; + + authSubscription: Subscription; + + protected constructor( + public dialogService: DialogService, + protected authService: AuthService, + protected currentUserService: CurrentUserService, + protected routingService: DataExplorerRoutingService, + ) {} + + ngOnInit() { + this.authSubscription = this.currentUserService.user$.subscribe( + user => { + this.hasDataExplorerWritePrivileges = this.authService.hasRole( + UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW, + ); + this.hasDataExplorerDeletePrivileges = this.authService.hasRole( + UserPrivilege.PRIVILEGE_DELETE_DATA_EXPLORER_VIEW, + ); + this.isAdmin = user.roles.indexOf(UserRole.ROLE_ADMIN) > -1; + this.afterInit(); + }, + ); + } + + abstract afterInit(): void; + + ngOnDestroy() { + this.authSubscription?.unsubscribe(); + } +} diff --git a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.html b/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.html deleted file mode 100644 index 0d04478e00..0000000000 --- a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.html +++ /dev/null @@ -1,230 +0,0 @@ - - - -
-
- - - -
-
- - - - - -
-
- - - - - - - - - - -
-
- -
- - -
- - -
-
- -
-

- This data view is empty and doesn't contain any widgets. -

- -
- - - - -
-
-
-
diff --git a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.ts b/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.ts deleted file mode 100644 index 5b33a4bfc1..0000000000 --- a/ui/src/app/data-explorer/components/panel/data-explorer-dashboard-panel.component.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Observable, of, Subscription, zip } from 'rxjs'; -import { DataExplorerDashboardGridComponent } from '../widget-view/grid-view/data-explorer-dashboard-grid.component'; -import { MatDrawer } from '@angular/material/sidenav'; -import { Tuple2 } from '../../../core-model/base/Tuple2'; -import { - ClientDashboardItem, - Dashboard, - DataExplorerWidgetModel, - DataLakeMeasure, - DataViewDataExplorerService, - TimeSettings, -} from '@streampipes/platform-services'; -import { DataExplorerDesignerPanelComponent } from '../designer-panel/data-explorer-designer-panel.component'; -import { TimeSelectionService } from '../../services/time-selection.service'; -import { AuthService } from '../../../services/auth.service'; -import { UserPrivilege } from '../../../_enums/user-privilege.enum'; -import { - ActivatedRoute, - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { DataExplorerDashboardSlideViewComponent } from '../widget-view/slide-view/data-explorer-dashboard-slide-view.component'; -import { - ConfirmDialogComponent, - CurrentUserService, - SpBreadcrumbService, -} from '@streampipes/shared-ui'; -import { MatDialog } from '@angular/material/dialog'; -import { map } from 'rxjs/operators'; -import { SpDataExplorerRoutes } from '../../data-explorer.routes'; - -@Component({ - selector: 'sp-data-explorer-dashboard-panel', - templateUrl: './data-explorer-dashboard-panel.component.html', - styleUrls: ['./data-explorer-dashboard-panel.component.scss'], -}) -export class DataExplorerDashboardPanelComponent implements OnInit, OnDestroy { - dashboardLoaded = false; - dashboard: Dashboard; - - /** - * This is the date range (start, end) to view the data and is set in data-explorer.ts - */ - timeSettings: TimeSettings; - viewMode = 'grid'; - - editMode = false; - timeRangeVisible = true; - - @ViewChild('dashboardGrid') - dashboardGrid: DataExplorerDashboardGridComponent; - - @ViewChild('dashboardSlide') - dashboardSlide: DataExplorerDashboardSlideViewComponent; - - @ViewChild('designerDrawer') - designerDrawer: MatDrawer; - - @ViewChild('designerPanel') - designerPanel: DataExplorerDesignerPanelComponent; - - hasDataExplorerWritePrivileges = false; - hasDataExplorerDeletePrivileges = false; - - public items: Dashboard[]; - - widgetIdsToRemove: string[] = []; - widgetsToUpdate: Map = new Map< - string, - DataExplorerWidgetModel - >(); - - currentlyConfiguredWidget: DataExplorerWidgetModel; - newWidgetMode = false; - currentlyConfiguredWidgetId: string; - dataLakeMeasure: DataLakeMeasure; - - showDesignerPanel = false; - showEditingHelpInfo = false; - - authSubscription: Subscription; - - constructor( - private dataViewDataExplorerService: DataViewDataExplorerService, - private dialog: MatDialog, - private timeSelectionService: TimeSelectionService, - private authService: AuthService, - private currentUserService: CurrentUserService, - private dashboardService: DataViewDataExplorerService, - private route: ActivatedRoute, - private dataViewService: DataViewDataExplorerService, - private router: Router, - private breadcrumbService: SpBreadcrumbService, - ) {} - - public ngOnInit() { - const params = this.route.snapshot.params; - const queryParams = this.route.snapshot.queryParams; - - const startTime = params.startTime; - const endTime = params.endTime; - - this.getDashboard(params.id, startTime, endTime); - - this.authSubscription = this.currentUserService.user$.subscribe(_ => { - this.hasDataExplorerWritePrivileges = this.authService.hasRole( - UserPrivilege.PRIVILEGE_WRITE_DATA_EXPLORER_VIEW, - ); - this.hasDataExplorerDeletePrivileges = this.authService.hasRole( - UserPrivilege.PRIVILEGE_DELETE_DATA_EXPLORER_VIEW, - ); - if ( - queryParams.action === 'edit' && - this.hasDataExplorerWritePrivileges - ) { - this.editMode = true; - } - }); - } - - ngOnDestroy() { - if (this.authSubscription) { - this.authSubscription.unsubscribe(); - } - } - - triggerResize() { - window.dispatchEvent(new Event('resize')); - } - - addWidget( - widgetConfig: Tuple2, - ): void { - this.dataLakeMeasure = widgetConfig.a; - this.dataViewDataExplorerService - .saveWidget(widgetConfig.b) - .subscribe(response => { - this.addWidgetToDashboard(response); - }); - } - - addWidgetToDashboard(widget: DataExplorerWidgetModel) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const dashboardItem = {} as ClientDashboardItem; - dashboardItem.id = widget.elementId; - dashboardItem.cols = 3; - dashboardItem.rows = 4; - dashboardItem.x = 0; - dashboardItem.y = 0; - this.dashboard.widgets.push(dashboardItem); - if (this.viewMode === 'grid') { - this.dashboardGrid.loadWidgetConfig(widget.elementId, true); - } else { - this.dashboardSlide.loadWidgetConfig(widget.elementId, true); - } - } - - persistDashboardChanges() { - this.dataViewDataExplorerService - .updateDashboard(this.dashboard) - .subscribe(result => { - this.dashboard.rev = result.rev; - if (this.widgetIdsToRemove.length > 0) { - const observables = this.deleteWidgets(); - zip(...observables).subscribe(() => { - this.widgetIdsToRemove.forEach(id => { - if (this.viewMode === 'grid') { - this.dashboardGrid.configuredWidgets.delete(id); - } else { - this.dashboardSlide.configuredWidgets.delete( - id, - ); - } - }); - - this.afterDashboardChange(); - }); - } else { - this.afterDashboardChange(); - } - }); - - this.editMode = false; - } - - afterDashboardChange() { - if (this.viewMode === 'grid') { - this.dashboardGrid.updateAllWidgets(); - } else { - this.dashboardSlide.updateAllWidgets(); - } - this.closeDesignerPanel(); - } - - startEditMode(widgetModel: DataExplorerWidgetModel) { - this.editMode = true; - this.updateCurrentlyConfiguredWidget(widgetModel); - this.showEditingHelpInfo = false; - } - - removeAndQueueItemForDeletion(widget: DataExplorerWidgetModel) { - const index = this.dashboard.widgets.findIndex( - item => item.id === widget.elementId, - ); - this.dashboard.widgets.splice(index, 1); - this.widgetIdsToRemove.push(widget.elementId); - if (this.currentlyConfiguredWidget.elementId === widget.elementId) { - this.currentlyConfiguredWidget = undefined; - } - } - - updateAndQueueItemForDeletion(widget: DataExplorerWidgetModel) { - this.widgetsToUpdate.set(widget.elementId, widget); - } - - deleteWidgets(): Observable[] { - return this.widgetIdsToRemove.map(widgetId => { - return this.dataViewDataExplorerService.deleteWidget(widgetId); - }); - } - - updateDateRange(timeSettings: TimeSettings) { - this.timeSettings = timeSettings; - this.dashboard.dashboardTimeSettings = timeSettings; - this.timeSelectionService.notify(timeSettings); - } - - updateCurrentlyConfiguredWidget(currentWidget: DataExplorerWidgetModel) { - if (currentWidget) { - this.widgetsToUpdate.set(currentWidget.elementId, currentWidget); - this.currentlyConfiguredWidget = currentWidget; - this.currentlyConfiguredWidgetId = currentWidget.elementId; - this.designerPanel.modifyWidgetMode(currentWidget, false); - this.showDesignerPanel = true; - } else { - this.showDesignerPanel = false; - } - } - - discardChanges() { - this.editMode = false; - } - - triggerEditMode() { - this.showEditingHelpInfo = false; - if (this.dashboard.widgets.length > 0) { - this.currentlyConfiguredWidgetId = this.dashboard.widgets[0].id; - const currentView = this.dashboardGrid - ? this.dashboardGrid - : this.dashboardSlide; - currentView.selectFirstWidgetForEditing( - this.currentlyConfiguredWidgetId, - ); - } else { - this.editMode = true; - this.createWidget(); - } - } - - createWidget() { - this.dataLakeMeasure = new DataLakeMeasure(); - this.currentlyConfiguredWidget = new DataExplorerWidgetModel(); - this.currentlyConfiguredWidget['@class'] = - 'org.apache.streampipes.model.datalake.DataExplorerWidgetModel'; - this.currentlyConfiguredWidget.baseAppearanceConfig = {}; - this.currentlyConfiguredWidget.baseAppearanceConfig.widgetTitle = - 'New Widget'; - this.currentlyConfiguredWidget.dataConfig = {}; - this.currentlyConfiguredWidget.dataConfig.ignoreMissingValues = false; - this.currentlyConfiguredWidget.baseAppearanceConfig.backgroundColor = - '#FFFFFF'; - this.currentlyConfiguredWidget.baseAppearanceConfig.textColor = - '#3e3e3e'; - this.currentlyConfiguredWidget = { ...this.currentlyConfiguredWidget }; - this.newWidgetMode = true; - this.showDesignerPanel = true; - this.newWidgetMode = true; - if (this.designerPanel) { - this.designerPanel.resetIndex(); - } - } - - closeDesignerPanel() { - this.showDesignerPanel = false; - this.currentlyConfiguredWidget = undefined; - this.dataLakeMeasure = undefined; - this.currentlyConfiguredWidgetId = undefined; - } - - deleteDashboard(dashboard: Dashboard) { - this.dashboardService.deleteDashboard(dashboard).subscribe(_ => { - this.goBackToOverview(); - }); - } - - getDashboard(dashboardId: string, startTime: number, endTime: number) { - this.dataViewService.getDataViews().subscribe(data => { - this.dashboard = data.filter( - dashboard => dashboard.elementId === dashboardId, - )[0]; - this.breadcrumbService.updateBreadcrumb( - this.breadcrumbService.makeRoute( - [SpDataExplorerRoutes.BASE], - this.dashboard.name, - ), - ); - this.viewMode = - this.dashboard.dashboardGeneralSettings.defaultViewMode || - 'grid'; - this.timeSettings = - startTime && endTime - ? this.overrideTime(+startTime, +endTime) - : this.dashboard.dashboardTimeSettings; - if (this.dashboard.widgets.length === 0 && this.editMode) { - this.triggerEditMode(); - } else if (this.dashboard.widgets.length === 0 && !this.editMode) { - this.showEditingHelpInfo = true; - } - this.dashboardLoaded = true; - }); - } - - overrideTime(startTime: number, endTime: number): TimeSettings { - return { startTime, endTime, dynamicSelection: -1 }; - } - - goBackToOverview() { - this.router.navigate(['dataexplorer']); - } - - confirmLeaveDashboard( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable { - if (this.editMode) { - const dialogRef = this.dialog.open(ConfirmDialogComponent, { - width: '500px', - data: { - title: 'Save changes?', - subtitle: - 'Update all changes to dashboard widgets or discard current changes.', - cancelTitle: 'Discard changes', - okTitle: 'Update', - confirmAndCancel: true, - }, - }); - return dialogRef.afterClosed().pipe( - map(shouldUpdate => { - if (shouldUpdate) { - this.persistDashboardChanges(); - } - return true; - }), - ); - } else { - return of(true); - } - } -} diff --git a/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html new file mode 100644 index 0000000000..eb51825326 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.html @@ -0,0 +1,82 @@ + + +
+
+ + + + + + + + +
+
+ +
+
diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss similarity index 87% rename from ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss rename to ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss index fb24a7dc6f..aef8593a37 100644 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.scss +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.scss @@ -55,3 +55,18 @@ .mr--5 { margin-right: -5px; } + +.formatted-date { + font-weight: bolder; +} + +.formatted-time { + font-weight: lighter; +} + +.formatted-datetime { + display: inline; + background: var(--color-bg-2); + border-radius: 5px; + padding: 5px; +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts new file mode 100644 index 0000000000..a2b663d5b3 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-range-selector.component.ts @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import { + TimeSelectionId, + TimeSettings, + TimeString, +} from '@streampipes/platform-services'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { TimeSelectionService } from '../../services/time-selection.service'; +import { TimeRangeSelectorMenuComponent } from './time-selector-menu/time-selector-menu.component'; + +@Component({ + selector: 'sp-time-range-selector', + templateUrl: 'time-range-selector.component.html', + styleUrls: ['./time-range-selector.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class TimeRangeSelectorComponent implements OnInit, OnChanges { + @ViewChild('menuTrigger') menu: MatMenuTrigger; + @ViewChild('timeSelectorMenu') + timeSelectorMenu: TimeRangeSelectorMenuComponent; + + @Output() dateRangeEmitter = new EventEmitter(); + + @Input() + timeSettings: TimeSettings; + + @Input() + showTimeSelector = true; + + simpleTimeString: string = ''; + timeString: TimeString; + timeStringMode: 'simple' | 'advanced' = 'simple'; + + constructor(private timeSelectionService: TimeSelectionService) {} + + ngOnInit() { + this.createDateString(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.timeSettings) { + this.createDateString(); + } + } + + applyPreviousInterval(): void { + this.changeTimeByInterval((a, b) => a - b); + } + + applyNextInterval(): void { + this.changeTimeByInterval((a, b) => a + b); + } + + compare(newDateRange: TimeSettings, oldDateRange: TimeSettings): boolean { + return ( + newDateRange && + oldDateRange && + newDateRange.startTime === oldDateRange.startTime && + newDateRange.endTime === oldDateRange.endTime && + newDateRange.dynamicSelection === oldDateRange.dynamicSelection + ); + } + + reloadData() { + this.dateRangeEmitter.emit(this.timeSettings); + } + + updateTimeSettingsAndReload() { + this.timeSelectionService.updateTimeSettings( + this.timeSettings, + new Date(), + ); + if (this.showTimeSelector) { + this.timeSelectorMenu.triggerDisplayUpdate(); + } + this.reloadData(); + } + + private changeTimeByInterval(func: (a: number, b: number) => number) { + const difference = + this.timeSettings.endTime - this.timeSettings.startTime; + const newStartTime = func(this.timeSettings.startTime, difference); + const newEndTime = func(this.timeSettings.endTime, difference); + + this.timeSettings.startTime = newStartTime; + this.timeSettings.endTime = newEndTime; + this.timeSettings.timeSelectionId = TimeSelectionId.CUSTOM; + this.timeSelectorMenu.triggerDisplayUpdate(); + this.createDateString(); + this.reloadData(); + } + + applyCurrentDateRange(timeSettings: TimeSettings) { + this.timeSettings = timeSettings; + this.createDateString(); + this.menu.closeMenu(); + this.reloadData(); + } + + createDateString(): void { + if (this.timeSettings.timeSelectionId !== TimeSelectionId.CUSTOM) { + this.simpleTimeString = this.timeSelectionService.getTimeSelection( + this.timeSettings.timeSelectionId, + ).label; + this.timeStringMode = 'simple'; + } else { + const startDate = new Date(this.timeSettings.startTime); + const endDate = new Date(this.timeSettings.endTime); + this.timeString = { + startDate: startDate.toLocaleDateString(), + endDate: endDate.toLocaleDateString(), + startTime: startDate.toLocaleTimeString(), + endTime: endDate.toLocaleTimeString(), + }; + this.timeStringMode = 'advanced'; + } + } +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html new file mode 100644 index 0000000000..9e277f0ca6 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.html @@ -0,0 +1,87 @@ + + +
+
+ + + + + +
+
+
+ {{ currentStartDate }} +
+
+ +
+
+
+
+ {{ currentEndDate }} +
+
+ +
+
+
+
+
+ +
+
diff --git a/ui/src/app/data-explorer/services/refresh-dashboard.service.ts b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss similarity index 70% rename from ui/src/app/data-explorer/services/refresh-dashboard.service.ts rename to ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss index 8506b9c51b..ce53cf59ea 100644 --- a/ui/src/app/data-explorer/services/refresh-dashboard.service.ts +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.scss @@ -16,14 +16,7 @@ * */ -import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; - -@Injectable({ providedIn: 'root' }) -export class RefreshDashboardService { - public refreshSubject: Subject = new Subject(); - - public notify(currentDashboard: string): void { - this.refreshSubject.next(currentDashboard); - } +.time-selector { + font-family: inherit; + font-size: 11pt; } diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts new file mode 100644 index 0000000000..2129a521c9 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/custom-time-range-selection/custom-time-range-selection.component.ts @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { TimeSelectionId, TimeSettings } from '@streampipes/platform-services'; +import { + DateRange, + DefaultMatCalendarRangeStrategy, + MatRangeDateSelectionModel, +} from '@angular/material/datepicker'; + +@Component({ + selector: 'sp-custom-time-range-selection', + templateUrl: 'custom-time-range-selection.component.html', + styleUrls: ['./custom-time-range-selection.component.scss'], +}) +export class CustomTimeRangeSelectionComponent implements OnInit { + @Input() timeSettings: TimeSettings; + @Output() timeSettingsEmitter = new EventEmitter(); + + currentStartDate: string; + currentEndDate: string; + currentStartTime: string; + currentEndTime: string; + currentDateRange: DateRange; + dateSelectionComplete = false; + + constructor( + private readonly selectionModel: MatRangeDateSelectionModel, + private readonly selectionStrategy: DefaultMatCalendarRangeStrategy, + ) {} + + ngOnInit(): void { + this.initializeDateRange(); + this.triggerDisplayUpdate(); + this.dateSelectionComplete = true; + } + + initializeDateRange(): void { + this.currentDateRange = new DateRange( + new Date(this.timeSettings.startTime), + new Date(this.timeSettings.endTime), + ); + } + + triggerDisplayUpdate() { + this.updateDateStrings(); + this.updateTimeStrings(); + } + + updateTimeStrings(): void { + this.currentStartTime = this.formatTime(this.currentDateRange.start); + this.currentEndTime = this.formatTime(this.currentDateRange.end); + } + + formatTime(date: Date): string { + return date.toTimeString().slice(0, 8); + } + + updateDateStrings(): void { + this.currentStartDate = this.formatDate(this.currentDateRange.start); + this.currentEndDate = this.formatDate(this.currentDateRange.end); + } + + formatDate(date: Date): string { + return date?.toLocaleDateString() || '-'; + } + + onDateChange(selectedDate: Date): void { + const newSelection = this.selectionStrategy.selectionFinished( + selectedDate, + this.selectionModel.selection, + ); + this.selectionModel.updateSelection(newSelection, this); + this.currentDateRange = new DateRange( + newSelection.start, + newSelection.end, + ); + this.dateSelectionComplete = this.selectionModel.isComplete(); + this.updateDateStrings(); + } + + saveSelection(): void { + this.updateDateTime(this.currentDateRange.start, this.currentStartTime); + this.updateDateTime(this.currentDateRange.end, this.currentEndTime); + this.timeSettings.startTime = this.currentDateRange.start.getTime(); + this.timeSettings.endTime = this.currentDateRange.end.getTime(); + this.timeSettings.timeSelectionId = TimeSelectionId.CUSTOM; + this.timeSettingsEmitter.emit(this.timeSettings); + } + + updateDateTime(date: Date, time: string): void { + const [hours, minutes, seconds] = time.split(':').map(Number); + date.setHours(hours, minutes, seconds || 0); + } +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html new file mode 100644 index 0000000000..70ded63676 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.html @@ -0,0 +1,53 @@ + + + diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss new file mode 100644 index 0000000000..b9c614c4d3 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.scss @@ -0,0 +1,44 @@ +/* + * 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. + * + */ + +.menu-content { + padding: 10px; + min-width: 550px; +} + +.filter-header { + font-weight: bold; + white-space: nowrap; + font-size: 12pt; +} + +.filter-section { + padding: 5px 10px 5px 5px; + border: 1px solid var(--color-bg-3); + background: var(--color-bg-1); +} + +.quick-link { + color: var(--color-accent); + cursor: pointer; + font-size: 11pt; +} + +.quick-link:hover { + font-weight: bold; +} diff --git a/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts new file mode 100644 index 0000000000..a24958bb31 --- /dev/null +++ b/ui/src/app/data-explorer/components/time-selector/time-selector-menu/time-selector-menu.component.ts @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { + QuickTimeSelection, + TimeSettings, +} from '@streampipes/platform-services'; +import { TimeSelectionService } from '../../../services/time-selection.service'; +import { CustomTimeRangeSelectionComponent } from './custom-time-range-selection/custom-time-range-selection.component'; + +@Component({ + selector: 'sp-time-selector-menu', + templateUrl: 'time-selector-menu.component.html', + styleUrls: ['./time-selector-menu.component.scss'], +}) +export class TimeRangeSelectorMenuComponent implements OnInit { + @Input() + timeSettings: TimeSettings; + + @Output() + timeSettingsEmitter: EventEmitter = + new EventEmitter(); + + quickSelections: QuickTimeSelection[] = []; + + @ViewChild('timeRangeSelection') + timeRangeSelection: CustomTimeRangeSelectionComponent; + + constructor(private timeSelectionService: TimeSelectionService) {} + + ngOnInit(): void { + this.quickSelections = this.timeSelectionService.quickTimeSelections; + } + + applyQuickSelection(quickSelection: QuickTimeSelection): void { + const selectedDateRange = + this.timeSelectionService.getDateRange(quickSelection); + this.timeSettings.timeSelectionId = quickSelection.timeSelectionId; + this.timeSettings.startTime = selectedDateRange.startDate.getTime(); + this.timeSettings.endTime = selectedDateRange.endDate.getTime(); + this.timeRangeSelection.initializeDateRange(); + this.triggerDisplayUpdate(); + this.timeSettingsEmitter.emit(this.timeSettings); + } + + triggerDisplayUpdate(): void { + this.timeRangeSelection.initializeDateRange(); + this.timeRangeSelection.triggerDisplayUpdate(); + } +} diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html b/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html deleted file mode 100644 index 29073a02a0..0000000000 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
-
- -
-
- - - - - - - - -
-
- -
-
diff --git a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts b/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts deleted file mode 100644 index 6dcea2232c..0000000000 --- a/ui/src/app/data-explorer/components/time-selector/timeRangeSelector.component.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { - Component, - EventEmitter, - Input, - OnInit, - Output, - ViewEncapsulation, -} from '@angular/core'; -import { TimeSettings } from '@streampipes/platform-services'; - -@Component({ - selector: 'sp-time-range-selector', - templateUrl: 'timeRangeSelector.component.html', - styleUrls: ['./timeRangeSelector.component.scss'], - encapsulation: ViewEncapsulation.None, -}) -export class TimeRangeSelectorComponent implements OnInit { - @Output() dateRangeEmitter = new EventEmitter(); - - _dateRange: TimeSettings; - - startDate: Date; - endDate: Date; - - public possibleTimeButtons = [ - { value: '15 min', offset: 15 }, - { value: '1 hour', offset: 60 }, - { value: '1 day', offset: 1440 }, - { value: '1 week', offset: 10080 }, - { value: '1 month', offset: 43200 }, - { value: '1 year', offset: 525600 }, - { value: 'custom', offset: -1 }, - ]; - - public selectedTimeButton; - - constructor() {} - - ngOnInit() { - if (!this.dateRange.startTime) { - this.setCurrentDateRange(this.possibleTimeButtons[0]); - } else if (this.dateRange.dynamicSelection !== -1) { - this.setCurrentDateRange( - this.possibleTimeButtons.find( - tb => tb.offset === this.dateRange.dynamicSelection, - ), - ); - } else { - this.startDate = new Date(this._dateRange.startTime); - this.endDate = new Date(this._dateRange.endTime); - } - } - - @Input() - set dateRange(dateRange: TimeSettings) { - if (!this.compare(dateRange, this._dateRange)) { - this._dateRange = dateRange; - this.updateTimeSettings(); - } - } - - get dateRange(): TimeSettings { - return this._dateRange; - } - - compare(newDateRange: TimeSettings, oldDateRange: TimeSettings): boolean { - return ( - newDateRange && - oldDateRange && - newDateRange.startTime === oldDateRange.startTime && - newDateRange.endTime === oldDateRange.endTime && - newDateRange.dynamicSelection === oldDateRange.dynamicSelection - ); - } - - updateTimeSettings() { - this.startDate = new Date(this.dateRange.startTime); - this.endDate = new Date(this.dateRange.endTime); - this.selectedTimeButton = this.findOffset( - this.dateRange.dynamicSelection, - ); - this.reloadData(); - } - - findOffset(dynamicSelection: number) { - return ( - this.possibleTimeButtons.find( - el => el.offset === dynamicSelection, - ) || this.possibleTimeButtons[0] - ); - } - - reloadData() { - this.dateRangeEmitter.emit(this.dateRange); - } - - increaseTime() { - this.changeTimeByInterval((a, b) => a + b); - } - - decreaseTime() { - this.changeTimeByInterval((a, b) => a - b); - } - - refreshData() { - const difference = this.endDate.getTime() - this.startDate.getTime(); - - const current = new Date().getTime(); - this.dateRange = { - startTime: current - difference, - endTime: current, - dynamicSelection: this.dateRange.dynamicSelection, - }; - - this.reloadData(); - } - - private changeTimeByInterval(func) { - const difference = this.endDate.getTime() - this.startDate.getTime(); - const newStartTime = func(this.startDate.getTime(), difference); - const newEndTime = func(this.endDate.getTime(), difference); - - this.startDate = new Date(newStartTime); - this.endDate = new Date(newEndTime); - this.selectedTimeButton = - this.possibleTimeButtons[this.possibleTimeButtons.length - 1]; - this.dateRange = { - startTime: newStartTime, - endTime: newEndTime, - dynamicSelection: -1, - }; - } - - changeCustomDateRange() { - this.selectedTimeButton = - this.possibleTimeButtons[this.possibleTimeButtons.length - 1]; - const newStartTime = this.startDate.getTime(); - const newEndTime = this.endDate.getTime(); - - this.dateRange = { - startTime: newStartTime, - endTime: newEndTime, - dynamicSelection: -1, - }; - } - - /** - * Sets the current date range from now to the value of offset in the past - * @param offset in minutes - */ - setCurrentDateRange(item) { - this.selectedTimeButton = item; - const current = new Date().getTime(); - this.startDate = new Date(current - item.offset * 60000); - this.endDate = new Date(current); - this.dateRange = { - startTime: this.startDate.getTime(), - endTime: this.endDate.getTime(), - dynamicSelection: item.offset, - }; - } -} diff --git a/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts b/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts index be0a6b300e..013b4ae9b9 100644 --- a/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts +++ b/ui/src/app/data-explorer/components/widget-view/abstract-widget-view.directive.ts @@ -56,12 +56,7 @@ export abstract class AbstractWidgetViewDirective { @Input() timeSettings: TimeSettings; - @Output() deleteCallback: EventEmitter = - new EventEmitter(); - @Output() updateCallback: EventEmitter = - new EventEmitter(); - @Output() configureWidgetCallback: EventEmitter = - new EventEmitter(); + @Output() deleteCallback: EventEmitter = new EventEmitter(); @Output() startEditModeEmitter: EventEmitter = new EventEmitter(); @@ -71,17 +66,6 @@ export abstract class AbstractWidgetViewDirective { protected widgetRegistryService: DataExplorerWidgetRegistry, ) {} - updateAllWidgets() { - this.configuredWidgets.forEach((value, key) => { - this.dataViewDataExplorerService - .updateWidget(value) - .subscribe(response => { - value.rev = response._rev; - this.currentlyConfiguredWidgetId = undefined; - }); - }); - } - startEditMode(value: DataExplorerWidgetModel) { this.startEditModeEmitter.emit(value); this.currentlyConfiguredWidgetId = value.elementId; @@ -105,13 +89,6 @@ export abstract class AbstractWidgetViewDirective { this.processWidget(r); this.onWidgetsAvailable(); this.widgetsAvailable = true; - if (this.dashboard.widgets.length > 0 && this.editMode) { - this.startEditModeEmitter.emit( - this.configuredWidgets.get( - this.dashboard.widgets[0].id, - ), - ); - } }); }); } @@ -150,16 +127,11 @@ export abstract class AbstractWidgetViewDirective { ); } - propagateItemRemoval(widget: DataExplorerWidgetModel) { - this.deleteCallback.emit(widget); - } - - propagateItemUpdate(dashboardWidget: DataExplorerWidgetModel) { - this.updateCallback.emit(dashboardWidget); + propagateItemRemoval(widgetIndex: number) { + this.deleteCallback.emit(widgetIndex); } propagateWidgetSelection(configuredWidget: DataExplorerWidgetModel) { - this.configureWidgetCallback.emit(configuredWidget); if (configuredWidget) { this.currentlyConfiguredWidgetId = configuredWidget.elementId; } else { @@ -168,10 +140,6 @@ export abstract class AbstractWidgetViewDirective { this.onOptionsChanged(); } - selectFirstWidgetForEditing(widgetId: string): void { - this.startEditModeEmitter.emit(this.configuredWidgets.get(widgetId)); - } - abstract onOptionsChanged(): void; abstract onWidgetsAvailable(): void; diff --git a/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html b/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html index 59cd269f65..46abe817da 100644 --- a/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html +++ b/ui/src/app/data-explorer/components/widget-view/grid-view/data-explorer-dashboard-grid.component.html @@ -30,16 +30,17 @@

{{ dashboard.description }}

diff --git a/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.html b/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.html index 9331289194..4c73a94118 100644 --- a/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.html +++ b/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.html @@ -53,16 +53,17 @@ height: gridsterItemComponent.height - 15 + 'px' }" [timeSettings]="timeSettings" - (updateCallback)="propagateItemUpdate($event)" + [globalTimeEnabled]=" + dashboard.dashboardGeneralSettings.globalTimeEnabled + " (deleteCallback)="propagateItemRemoval($event)" - (configureWidgetCallback)="propagateWidgetSelection($event)" (startEditModeEmitter)="startEditMode($event)" [dashboardItem]="currentDashboardItem" [configuredWidget]="currentWidget" [dataLakeMeasure]="currentMeasure" - [currentlyConfiguredWidgetId]="currentlyConfiguredWidgetId" [editMode]="editMode" [gridMode]="false" + [widgetIndex]="i" [gridsterItemComponent]="gridsterItemComponent" *ngIf=" widgetsAvailable && diff --git a/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.ts b/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.ts index 4c762a510f..b6effac6a4 100644 --- a/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.ts +++ b/ui/src/app/data-explorer/components/widget-view/slide-view/data-explorer-dashboard-slide-view.component.ts @@ -70,12 +70,6 @@ export class DataExplorerDashboardSlideViewComponent index ] as unknown as DashboardItem; this.currentlyConfiguredWidgetId = widgetId; - - // Opens the design panel for the current widget when in edit mode - if (this.editMode) { - this.startEditModeEmitter.emit(this.currentWidget); - } - this.displayWidget = true; }); } diff --git a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html index f2a051edab..fa9617a0ed 100644 --- a/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html +++ b/ui/src/app/data-explorer/components/widget/data-explorer-dashboard-widget.component.html @@ -24,11 +24,8 @@ color: configuredWidget.baseAppearanceConfig.textColor, height: gridsterItemComponent.height - 13 + 'px', border: - editMode && - currentlyConfiguredWidgetId === configuredWidget.elementId - ? '4px solid var(--color-accent)' - : '2px solid ' + - configuredWidget.baseAppearanceConfig.backgroundColor + '2px solid ' + + configuredWidget.baseAppearanceConfig.backgroundColor }" [attr.data-cy]=" 'widget-' + configuredWidget.baseAppearanceConfig.widgetTitle @@ -68,7 +65,7 @@ [matMenuTriggerFor]="menu" aria-label="More options" matTooltip="More options" - *ngIf="!editMode" + *ngIf="!editMode && !dataViewMode" [attr.data-cy]=" 'more-options-' + configuredWidget.baseAppearanceConfig.widgetTitle.replaceAll( @@ -114,28 +111,6 @@ > get_app -