From 14b8bbe6c98823ccd581fe163c3c524175f0f97f Mon Sep 17 00:00:00 2001 From: mtien Date: Mon, 19 Apr 2021 11:03:29 -0700 Subject: [PATCH 01/10] NIFIREG-395 - [WIP] Implemented the ability to import and export versioned flows through the UI. - Added REST endpoints in BucketFlowResource for importFlow, importVersionedFlow, exportVersionedFlow. - Added import and export dialogs. --- .../nifi-registry-web-api/pom.xml | 10 + .../registry/web/api/BucketFlowResource.java | 223 +++++++++++++++++- .../nf-registry-download-version.html | 53 +++++ .../nf-registry-download-versioned-flow.js | 96 ++++++++ .../nf-registry-import-new-flow.html | 135 +++++++++++ .../nf-registry-import-new-flow.js | 182 ++++++++++++++ .../nf-registry-import-versioned-flow.html | 120 ++++++++++ .../nf-registry-import-versioned-flow.js | 155 ++++++++++++ .../nf-registry-grid-list-viewer.html | 22 +- .../src/main/webapp/nf-registry.module.js | 13 +- .../main/webapp/services/nf-registry.api.js | 107 +++++++++ .../webapp/services/nf-registry.service.js | 198 ++++++++++++---- .../services/nf-registry.service.spec.js | 2 +- .../explorer/dialogs/_structureElements.scss | 162 +++++++++++++ .../src/main/webapp/theming/nf-registry.scss | 1 + 15 files changed, 1427 insertions(+), 52 deletions(-) create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml index 1fd4354d6..444920f79 100644 --- a/nifi-registry-core/nifi-registry-web-api/pom.xml +++ b/nifi-registry-core/nifi-registry-web-api/pom.xml @@ -423,6 +423,16 @@ jjwt 0.7.0 + + com.fasterxml.jackson.core + jackson-databind + + + com.google.protobuf + protobuf-java + 3.11.4 + compile + org.apache.nifi.registry diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 8dd729043..a3d542f9f 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.registry.web.api; +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -24,6 +25,10 @@ import io.swagger.annotations.Authorization; import io.swagger.annotations.Extension; import io.swagger.annotations.ExtensionProperty; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.registry.bucket.BucketItem; import org.apache.nifi.registry.diff.VersionedFlowDifference; @@ -36,7 +41,9 @@ import org.apache.nifi.registry.revision.web.ClientIdParameter; import org.apache.nifi.registry.revision.web.LongParameter; import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider; import org.apache.nifi.registry.web.service.ServiceFacade; +import org.glassfish.jersey.media.multipart.FormDataParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -56,6 +63,7 @@ import javax.ws.rs.core.Response; import java.util.List; import java.util.SortedSet; +import org.springframework.web.util.UriUtils; @Component @Path("/buckets/{bucketId}/flows") @@ -66,11 +74,15 @@ ) public class BucketFlowResource extends ApplicationResource { + public static final String INVALID_JSON_MESSAGE = "Deserialization of uploaded JSON failed"; + @Autowired public BucketFlowResource(final ServiceFacade serviceFacade, final EventService eventService) { super(serviceFacade, eventService); } + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperProvider.getMapper(); + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -99,8 +111,8 @@ public Response createFlow( verifyPathParamsMatchBody(bucketId, flow); - final VersionedFlow createdFlow = serviceFacade.createFlow(bucketId, flow); - publish(EventFactory.flowCreated(createdFlow)); + final VersionedFlow createdFlow = createAndPublishVersionedFlow(bucketId, flow); + return Response.status(Response.Status.OK).entity(createdFlow).build(); } @@ -291,6 +303,129 @@ public Response createFlowVersion( return Response.status(Response.Status.OK).entity(createdSnapshot).build(); } + @POST + @Path("{flowId}/versions/import") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Upload flow version", + notes = "Uploads the next version of a flow. The version number of the object being created must be the " + + "next available version integer. Flow versions are immutable after they are created.", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response importVersionedFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam(value = "The flow identifier") + final String flowId, + @FormDataParam("file") final InputStream in, + @FormDataParam("comments") final String comments) { + + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("The bucket identifier is required."); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("The flow identifier is required."); + } + + // deserialize InputStream to a VersionedFlowSnapshot + VersionedFlowSnapshot versionedFlowSnapshot; + + versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); + + // clear or set the necessary snapShotMetadata + if (versionedFlowSnapshot.getSnapshotMetadata() != null) { + versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setLink(null); + versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); + versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); + // if there are new comments, then set it + // otherwise, keep the original comments + if (!StringUtils.isBlank(comments)) { + versionedFlowSnapshot.getSnapshotMetadata().setComments(comments); + } + } + + return createFlowVersion(bucketId, flowId, versionedFlowSnapshot); + } + + @POST + @Path("import") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create flow", + notes = "Creates a flow in the given bucket. The flow id is created by the server and populated in the returned entity.", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}")}) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)}) + public Response importFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") final String bucketId, + @FormDataParam("file") final InputStream in, + @FormDataParam("name") final String name, + @FormDataParam("description") final String description) { + + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("The bucket identifier is required."); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("The flow name is required."); + } + + // create VersionedFlow + final VersionedFlow versionedFlow = new VersionedFlow(); + + versionedFlow.setName(name); + versionedFlow.setRevision(new RevisionInfo(null, 0L)); + if (!StringUtils.isBlank(description)) { + versionedFlow.setDescription(description); + } + + final VersionedFlow createdFlow = createAndPublishVersionedFlow(bucketId, versionedFlow); + + // deserialize InputStream and create new VersionedFlowSnapshot + final VersionedFlowSnapshot versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); + + setSnaphotMetadataIfMissing(bucketId, createdFlow.getIdentifier(), versionedFlowSnapshot); + + // set remaining snapshot metadata + final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + versionedFlowSnapshot.getSnapshotMetadata().setAuthor(userIdentity); + versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); + + final VersionedFlowSnapshot createdSnapshot = serviceFacade.createFlowSnapshot(versionedFlowSnapshot); + publish(EventFactory.flowVersionCreated(createdSnapshot)); + + return Response.status(Response.Status.OK).entity(createdSnapshot).build(); + } + @GET @Path("{flowId}/versions") @Consumes(MediaType.WILDCARD) @@ -385,6 +520,64 @@ public Response getLatestFlowVersionMetadata( return Response.status(Response.Status.OK).entity(latest).build(); } + @GET + @Path("{flowId}/versions/{versionNumber: \\d+}/export") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Exports specified bucket flow version content", + notes = "Exports the specified version of a flow, including the metadata and content of the flow.", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}")}) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)}) + public Response exportVersionedFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") final String flowId, + @PathParam("versionNumber") + @ApiParam("The version number") final Integer versionNumber) { + + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("The bucket identifier is required."); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("The flow identifier is required."); + } + + if (versionNumber == null) { + throw new IllegalArgumentException("The version number is required."); + } + + final VersionedFlowSnapshot versionedFlowSnapshot = serviceFacade.getFlowSnapshot(bucketId, flowId, versionNumber); + + versionedFlowSnapshot.setFlow(null); + versionedFlowSnapshot.setBucket(null); + versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setLink(null); + + String attachmentName = "flow-version-" + versionNumber; + + UriUtils.encodePath(attachmentName, StandardCharsets.UTF_8); + + String filename = attachmentName + ".json"; + + final String versionedFlowSnapshotJsonString = serializeToJson(versionedFlowSnapshot); + + return generateOkResponse(versionedFlowSnapshotJsonString).header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)).build(); + } + @GET @Path("{flowId}/versions/{versionNumber: \\d+}") @Consumes(MediaType.WILDCARD) @@ -539,4 +732,30 @@ private static void setSnaphotMetadataIfMissing( flowSnapshot.setSnapshotMetadata(metadata); } + + private String serializeToJson(final VersionedFlowSnapshot dto) { + try { + return OBJECT_MAPPER.writeValueAsString(dto); + } catch (IOException e) { + throw new IllegalArgumentException("Deserialization of selected flow failed", e); + } + } + + private static VersionedFlowSnapshot deserializeVersionedFlowSnapshot(@NotNull InputStream in) { + try { + return OBJECT_MAPPER.readValue(in, VersionedFlowSnapshot.class); + } catch (IOException e) { + throw new IllegalArgumentException(INVALID_JSON_MESSAGE, e); + } + } + + private VersionedFlow createAndPublishVersionedFlow( + @NotNull String bucketIdentifier, + @NotNull VersionedFlow versionedFlow) { + + final VersionedFlow createdFlow = serviceFacade.createFlow(bucketIdentifier, versionedFlow); + publish(EventFactory.flowCreated(createdFlow)); + + return createdFlow; + } } diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html new file mode 100644 index 000000000..1d24a7354 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html @@ -0,0 +1,53 @@ + + +
+
+ + Download Version + + +
+
+
+ +
+
+ + + + Latest version (v.{{snapshotMeta.version}}) + Version {{snapshotMeta.version}} + + + +
+
+
+ + + +
+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js new file mode 100644 index 000000000..b686ca9db --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js @@ -0,0 +1,96 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FdsSnackBarService } from '@nifi-fds/core'; + +/** + * NfRegistryDownloadVersionedFlow constructor. + * + * @param nfRegistryApi The api service. + * @param fdsSnackBarService The FDS snack bar service module. + * @param matDialogRef The angular material dialog ref. + * @param data The data passed into this component. + * @constructor + */ +function NfRegistryDownloadVersionedFlow(nfRegistryApi, fdsSnackBarService, matDialogRef, data) { + // Services + this.snackBarService = fdsSnackBarService; + this.nfRegistryApi = nfRegistryApi; + this.dialogRef = matDialogRef; + // local state + this.keepDialogOpen = false; + this.protocol = location.protocol; + this.droplet = data.droplet; + this.selectedVersion = this.droplet.snapshotMetadata[0].version; +} + +NfRegistryDownloadVersionedFlow.prototype = { + constructor: NfRegistryDownloadVersionedFlow, + + /** + * Download specified versioned flow snapshot. + */ + downloadVersion: function () { + var self = this; + var version = this.selectedVersion; + + this.nfRegistryApi.exportDropletVersionedSnapshot(this.droplet.link.href, version).subscribe(function (response) { + if (!response.status || response.status === 200) { + self.snackBarService.openCoaster({ + title: 'Success', + message: 'Exported flow.', + verticalPosition: 'bottom', + horizontalPosition: 'right', + icon: 'fa fa-check-circle-o', + color: '#1EB475', + duration: 3000 + }); + + if (self.keepDialogOpen !== true) { + self.dialogRef.close(); + } + } else { + self.dialogRef.close(); + } + }); + }, + + /** + * Cancel creation of a download version and close dialog. + */ + cancel: function () { + this.dialogRef.close(); + } +}; + +NfRegistryDownloadVersionedFlow.annotations = [ + new Component({ + templateUrl: './nf-registry-download-version.html' + }) +]; + +NfRegistryDownloadVersionedFlow.parameters = [ + NfRegistryApi, + FdsSnackBarService, + MatDialogRef, + MAT_DIALOG_DATA +]; + +export default NfRegistryDownloadVersionedFlow; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html new file mode 100644 index 000000000..ae0b1d17f --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html @@ -0,0 +1,135 @@ + + +
+
+ + Import New Flow + + +
+
+
+
+ +
+
+
+ +
+
+ v.1 +
+
+
+
+ +
+
+ +
+
+ +
+
+ + + + {{bucket.name}} + + + +
+
+
+ +
+ + + Looks good! + + + + + + File format is not valid + + +
+
+ + +
+ + Select file +
+
+
+ +
+
+
+
+
+
+
+ + + +
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js new file mode 100644 index 000000000..b0fcfff66 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js @@ -0,0 +1,182 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FdsSnackBarService } from '@nifi-fds/core'; + +/** + * NfRegistryImportNewFlow constructor. + * + * @param nfRegistryApi The api service. + * @param fdsSnackBarService The FDS snack bar service module. + * @param matDialogRef The angular material dialog ref. + * @param data The data passed into this component. + * @constructor + */ +function NfRegistryImportNewFlow(nfRegistryApi, fdsSnackBarService, matDialogRef, data) { + // Services + this.snackBarService = fdsSnackBarService; + this.nfRegistryApi = nfRegistryApi; + this.dialogRef = matDialogRef; + // local state + this.keepDialogOpen = false; + this.protocol = location.protocol; + this.buckets = data.buckets; + this.writableBuckets = []; + this.fileToUpload = null; + this.fileName = null; + this.name = ''; + this.description = ''; + this.selectedBucket = ''; + this.hoverValidity = ''; + this.extensions = 'application/json'; + this.multiple = false; +} + +NfRegistryImportNewFlow.prototype = { + constructor: NfRegistryImportNewFlow, + + ngOnInit: function () { + // only show buckets the user can write + var self = this; + self.writableBuckets = self.filterWritableBuckets(self.buckets); + }, + + //TODO: get from the service instead + filterWritableBuckets: function (buckets) { + var self = this; + self.writableBuckets = this.writableBuckets; + + buckets.forEach(function (b) { + if (b.permissions.canWrite) { + self.writableBuckets.push(b); + } + }); + return self.writableBuckets; + }, + + fileDragHandler: function (event, extensions) { + event.preventDefault(); + event.stopPropagation(); + + this.extensions = extensions; + + var {items} = event.dataTransfer; + this.hoverValidity = this.isFileInvalid(items) + ? 'invalid' + : 'valid'; + }, + + fileDragEndHandler: function () { + this.hoverValidity = ''; + }, + + fileDropHandler: function (event) { + event.preventDefault(); + event.stopPropagation(); + + var { files } = event.dataTransfer; + + if (!this.isFileInvalid(Array.from(files))) { + this.handleFileInput(files); + } + + this.hoverValidity = ''; + }, + + /** + * Handle the file input on change. + */ + handleFileInput: function (files) { + // get the file + this.fileToUpload = files[0]; + + // get the filename + var fileName = this.fileToUpload.name; + + // trim off the file extension + this.fileName = fileName.replace(/\..*/, ''); + }, + + /** + * Open the file selector. + */ + selectFile: function () { + document.getElementById('upload-flow-file-field').click(); + }, + + /** + * Upload new data flow snapshot. + */ + importNewFlow: function () { + var self = this; + self.name = this.name; + self.description = this.description; + self.selectedBucket = this.selectedBucket; + + this.nfRegistryApi.uploadFlow(self.selectedBucket.link.href, self.fileToUpload, self.name, self.description).subscribe(function (response) { + if (!response.status || response.status === 200) { + self.snackBarService.openCoaster({ + title: 'Success', + message: 'Successfully imported ' + response.flow.name + ' to the ' + response.bucket.name + ' bucket.', + verticalPosition: 'bottom', + horizontalPosition: 'right', + icon: 'fa fa-check-circle-o', + color: '#1EB475', + duration: 3000 + }); + + if (self.keepDialogOpen !== true) { + var uploadedFlowHref = response.flow.link.href; + self.dialogRef.close(uploadedFlowHref); + } + } else { + self.dialogRef.close(); + } + }); + }, + + isFileInvalid: function (items) { + return ((items.length > 1) || (this.extensions !== '' && (items[0].type === '')) || ((this.extensions.indexOf(items[0].type) === -1))); + }, + + /** + * Cancel uploading a new version and close dialog. + */ + cancel: function () { + this.dialogRef.close(); + }, + +}; + +NfRegistryImportNewFlow.annotations = [ + new Component({ + templateUrl: './nf-registry-import-new-flow.html' + }) +]; + +NfRegistryImportNewFlow.parameters = [ + NfRegistryApi, + FdsSnackBarService, + MatDialogRef, + MAT_DIALOG_DATA +]; + +export default NfRegistryImportNewFlow; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html new file mode 100644 index 000000000..27f3d3dca --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html @@ -0,0 +1,120 @@ + + +
+
+ + Import New Version + + +
+
+
+
+ +
+
+
+ +
+
+ v.{{droplet.versionCount + 1}} +
+
+
+
+ + + + Looks good! + + + + + + File format is not valid + + +
+
+ + +
+ + Select file +
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + + +
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js new file mode 100644 index 000000000..a3a1bd8a8 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js @@ -0,0 +1,155 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FdsSnackBarService } from '@nifi-fds/core'; + +/** + * NfRegistryImportVersionedFlow constructor. + * + * @param nfRegistryApi The api service. + * @param fdsSnackBarService The FDS snack bar service module. + * @param matDialogRef The angular material dialog ref. + * @param data The data passed into this component. + * @constructor + */ +function NfRegistryImportVersionedFlow(nfRegistryApi, fdsSnackBarService, matDialogRef, data) { + // Services + this.snackBarService = fdsSnackBarService; + this.nfRegistryApi = nfRegistryApi; + this.dialogRef = matDialogRef; + // local state + this.keepDialogOpen = false; + this.protocol = location.protocol; + this.droplet = data.droplet; + this.fileToUpload = null; + this.fileName = null; + this.comments = ''; + this.hoverValidity = ''; + this.extensions = 'application/json'; + this.multiple = false; +} + +NfRegistryImportVersionedFlow.prototype = { + constructor: NfRegistryImportVersionedFlow, + + fileDragHandler: function (event, extensions) { + event.preventDefault(); + event.stopPropagation(); + + this.extensions = extensions; + + var { items } = event.dataTransfer; + this.hoverValidity = this.isFileInvalid(items) + ? 'invalid' + : 'valid'; + }, + + fileDragEndHandler: function () { + this.hoverValidity = ''; + }, + + fileDropHandler: function (event) { + event.preventDefault(); + event.stopPropagation(); + + var { files } = event.dataTransfer; + + if (!this.isFileInvalid(Array.from(files))) { + this.handleFileInput(files); + } + + this.hoverValidity = ''; + }, + /** + * Handle the file input on change. + */ + handleFileInput: function (files) { + // get the file + this.fileToUpload = files[0]; + + // get the filename + var fileName = this.fileToUpload.name; + + // trim off the file extension + this.fileName = fileName.replace(/\..*/, ''); + }, + + /** + * Open the file selector. + */ + selectFile: function () { + document.getElementById('upload-versioned-flow-file-field').click(); + }, + + /** + * Upload new versioned flow snapshot. + */ + importNewVersion: function () { + var self = this; + var comments = this.comments; + + this.nfRegistryApi.uploadVersionedFlowSnapshot(this.droplet.link.href, this.fileToUpload, comments).subscribe(function (response) { + if (!response.status || response.status === 200) { + self.snackBarService.openCoaster({ + title: 'Success', + message: 'Successfully imported version ' + response.snapshotMetadata.version + ' of ' + response.flow.name + '.', + verticalPosition: 'bottom', + horizontalPosition: 'right', + icon: 'fa fa-check-circle-o', + color: '#1EB475', + duration: 3000 + }); + + if (self.keepDialogOpen !== true) { + self.dialogRef.close(); + } + } else { + self.dialogRef.close(); + } + }); + }, + + isFileInvalid: function (items) { + return (items.length > 1) || (this.extensions !== '' && items[0].type === '') || (this.extensions.indexOf(items[0].type) === -1); + }, + + /** + * Cancel uploading a new version and close dialog. + */ + cancel: function () { + this.dialogRef.close(); + }, + +}; + +NfRegistryImportVersionedFlow.annotations = [ + new Component({ + templateUrl: './nf-registry-import-versioned-flow.html' + }) +]; + +NfRegistryImportVersionedFlow.parameters = [ + NfRegistryApi, + FdsSnackBarService, + MatDialogRef, + MAT_DIALOG_DATA +]; + +export default NfRegistryImportVersionedFlow; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html index e0faa83c4..294fa56ab 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html @@ -17,17 +17,19 @@
-
- +
Sort by:
{{nfRegistryService.getSortByLabel()}}
+
+ +
+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js index 59c7262f3..a44f0a130 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js @@ -52,6 +52,9 @@ import { NfRegistryUsersAdministrationAuthGuard, NfRegistryWorkflowsAdministrationAuthGuard } from 'services/nf-registry.auth-guard.service'; +import NfRegistryImportVersionedFlow from './components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; +import NfRegistryImportNewFlow from './components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; +import NfRegistryDownloadVersionedFlow from './components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; function NfRegistryModule() { } @@ -89,7 +92,10 @@ NfRegistryModule.annotations = [ NfRegistryDropletGridListViewer, NfPageNotFoundComponent, NfLoginComponent, - NfUserLoginComponent + NfUserLoginComponent, + NfRegistryDownloadVersionedFlow, + NfRegistryImportVersionedFlow, + NfRegistryImportNewFlow ], entryComponents: [ NfRegistryAddUser, @@ -99,7 +105,10 @@ NfRegistryModule.annotations = [ NfRegistryAddUsersToGroup, NfRegistryAddPolicyToBucket, NfRegistryEditBucketPolicy, - NfUserLoginComponent + NfUserLoginComponent, + NfRegistryDownloadVersionedFlow, + NfRegistryImportVersionedFlow, + NfRegistryImportNewFlow ], providers: [ NfRegistryService, diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 99cacf8e3..a3f7c3ab8 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -75,6 +75,113 @@ NfRegistryApi.prototype = { ); }, + /** + * Retrieves the specified versioned flow snapshot for an existing droplet the registry has stored. + * + * @param {string} dropletUri The uri of the droplet to request. + * @param {number} versionNumber The version of the flow to request. + * @returns {*} + */ + exportDropletVersionedSnapshot: function (dropletUri, versionNumber) { + var self = this; + var url = '../nifi-registry-api/' + dropletUri; + url += '/versions/' + versionNumber + '/export'; + + return this.http.get(url, headers).pipe( + map(function (response) { + // export the VersionedFlowSnapshot + var element = document.createElement('a'); + element.setAttribute('href', url); + element.setAttribute('download', 'versionedFlow'); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); + + return response; + }), + catchError(function (error) { + self.dialogService.openConfirm({ + title: 'Error', + message: error.error, + acceptButton: 'Ok', + acceptButtonColor: 'fds-warn' + }); + return of(error); + }) + ); + }, + + /** + * Uploads a new versioned flow snapshot to the existing droplet the registry has stored. + * + * @param {string} dropletUri The uri of the droplet to request. + * @param file The file to be uploaded. + * @param {string} comments The optional comments. + * @returns {*} + */ + uploadVersionedFlowSnapshot: function (dropletUri, file, comments) { + var self = this; + var url = '../nifi-registry-api/' + dropletUri; + url += '/versions/import'; + + var formData = new FormData(); + formData.append('file', file, 'fileToUpload'); + formData.append('comments', comments); + + return this.http.post(url, formData, headers).pipe( + map(function (response) { + return response; + }), + catchError(function (error) { + self.dialogService.openConfirm({ + title: 'Error', + message: error.error, + acceptButton: 'Ok', + acceptButtonColor: 'fds-warn' + }); + return of(error); + }) + ); + }, + + /** + * Uploads a new flow to the existing droplet the registry has stored. + * + * @param {string} bucketUri The uri of the droplet to request. + * @param file The file to be uploaded. + * @param {string} name The flow name. + * @param {string} description The optional description. + * @returns {*} + */ + uploadFlow: function (bucketUri, file, name, description) { + var self = this; + var url = '../nifi-registry-api/' + bucketUri; + url += '/flows/import'; + + var formData = new FormData(); + formData.append('file', file, 'fileToUpload'); + formData.append('name', name); + formData.append('description', description); + + return this.http.post(url, formData, headers).pipe( + map(function (response) { + return response; + }), + catchError(function (error) { + self.dialogService.openConfirm({ + title: 'Error', + message: error.error, + acceptButton: 'Ok', + acceptButtonColor: 'fds-warn' + }); + return of(error); + }) + ); + }, + /** * Retrieves the given droplet with or without snapshot metadata. * diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js index 8e1241b79..099d7b60a 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js @@ -17,9 +17,13 @@ import { TdDataTableService } from '@covalent/core/data-table'; import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material'; import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core'; import NfRegistryApi from 'services/nf-registry.api.js'; import NfStorage from 'services/nf-storage.service.js'; +import NfRegistryDownloadVersionedFlow from '../components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; +import NfRegistryImportVersionedFlow from '../components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; +import NfRegistryImportNewFlow from '../components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; /** * NfRegistryService constructor. @@ -30,9 +34,10 @@ import NfStorage from 'services/nf-storage.service.js'; * @param router The angular router module. * @param fdsDialogService The FDS dialog service. * @param fdsSnackBarService The FDS snack bar service module. + * @param matDialog The angular material dialog module. * @constructor */ -function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, fdsDialogService, fdsSnackBarService) { +function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, fdsDialogService, fdsSnackBarService, matDialog) { var self = this; this.registry = { name: 'NiFi Registry', @@ -52,6 +57,7 @@ function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, this.dialogService = fdsDialogService; this.snackBarService = fdsSnackBarService; this.dataTableService = tdDataTableService; + this.matDialog = matDialog; // data table column definitions this.userColumns = [ @@ -133,9 +139,28 @@ function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, ]; this.dropletActions = [ { - name: 'Delete', + name: 'Import new version', + icon: 'fa fa-upload', + tooltip: 'Import new flow version', + disabled: function (droplet) { + return !droplet.permissions.canWrite; + } + }, + { + name: 'Download version', + icon: 'fa fa-download', + tooltip: 'Download flow version', + disabled: function (droplet) { + return false; + } + }, + { + name: 'Delete data flow', icon: 'fa fa-trash', - tooltip: 'Delete' + tooltip: 'Delete', + disabled: function (droplet) { + return !droplet.permissions.canDelete; + } } ]; this.disableMultiDeleteAction = false; @@ -393,6 +418,104 @@ NfRegistryService.prototype = { return label; }, + /** + * Delete the latest flow snapshot. + * + * @param droplet The droplet object. + */ + deleteDroplet: function (droplet) { + var self = this; + this.dialogService.openConfirm({ + title: 'Delete ' + droplet.type.toLowerCase(), + message: 'All versions of this ' + droplet.type.toLowerCase() + ' will be deleted.', + cancelButton: 'Cancel', + acceptButton: 'Delete', + acceptButtonColor: 'fds-warn' + }).afterClosed().subscribe( + function (accept) { + if (accept) { + var deleteUrl = droplet.link.href; + if (droplet.type === 'Flow') { + deleteUrl = deleteUrl + '?version=' + droplet.revision.version; + } + self.api.deleteDroplet(deleteUrl).subscribe(function (response) { + if (!response.status || response.status === 200) { + self.droplets = self.droplets.filter(function (d) { + return (d.identifier !== droplet.identifier); + }); + self.snackBarService.openCoaster({ + title: 'Success', + message: 'All versions of this ' + droplet.type.toLowerCase() + ' have been deleted.', + verticalPosition: 'bottom', + horizontalPosition: 'right', + icon: 'fa fa-check-circle-o', + color: '#1EB475', + duration: 3000 + }); + self.droplet = {}; + self.filterDroplets(); + } + }); + } + } + ); + }, + + /** + * Opens the download version dialog. + * + * @param droplet The droplet object. + */ + openDownloadVersionedFlowDialog: function (droplet) { + this.matDialog.open(NfRegistryDownloadVersionedFlow, { + disableClose: true, + width: '400px', + data: { + droplet: droplet + } + }); + }, + + /** + * Opens the import new flow dialog. + * + * @param buckets The buckets object. + */ + openImportNewFlowDialog: function (buckets) { + var self = this; + + this.matDialog.open(NfRegistryImportNewFlow, { + disableClose: true, + width: '550px', + data: { + buckets: buckets + } + }).afterClosed().subscribe(function (flowUri) { + if (flowUri != null) { + self.router.navigateByUrl('explorer/grid-list/' + flowUri); + } + }); + }, + + /** + * Opens the import new version dialog. + * + * @param droplet The droplet object. + */ + openImportVersionedFlowDialog: function (droplet) { + var self = this; + + this.matDialog.open(NfRegistryImportVersionedFlow, { + disableClose: true, + width: '550px', + data: { + droplet: droplet + } + }).afterClosed().subscribe(function () { + self.getDropletSnapshotMetadata(droplet); + }); + }, + /** * Execute the given droplet action. * @@ -400,42 +523,20 @@ NfRegistryService.prototype = { * @param droplet The droplet object the `action` will act upon. */ executeDropletAction: function (action, droplet) { - var self = this; - if (action.name.toLowerCase() === 'delete') { - this.dialogService.openConfirm({ - title: 'Delete ' + droplet.type.toLowerCase(), - message: 'All versions of this ' + droplet.type.toLowerCase() + ' will be deleted.', - cancelButton: 'Cancel', - acceptButton: 'Delete', - acceptButtonColor: 'fds-warn' - }).afterClosed().subscribe( - function (accept) { - if (accept) { - var deleteUrl = droplet.link.href; - if (droplet.type === 'Flow') { - deleteUrl = deleteUrl + '?version=' + droplet.revision.version; - } - self.api.deleteDroplet(deleteUrl).subscribe(function (response) { - if (!response.status || response.status === 200) { - self.droplets = self.droplets.filter(function (d) { - return (d.identifier !== droplet.identifier); - }); - self.snackBarService.openCoaster({ - title: 'Success', - message: 'All versions of this ' + droplet.type.toLowerCase() + ' have been deleted.', - verticalPosition: 'bottom', - horizontalPosition: 'right', - icon: 'fa fa-check-circle-o', - color: '#1EB475', - duration: 3000 - }); - self.droplet = {}; - self.filterDroplets(); - } - }); - } - } - ); + switch (action.name.toLowerCase()) { + case 'import new version': + // Opens the import versioned flow dialog + this.openImportVersionedFlowDialog(droplet); + break; + case 'download version': + // Opens the download flow version dialog + this.openDownloadVersionedFlowDialog(droplet); + break; + case 'delete data flow': + // Deletes the entire data flow + this.deleteDroplet(droplet); + break; + default: // do nothing } }, @@ -633,6 +734,22 @@ NfRegistryService.prototype = { this.getAutoCompleteBuckets(); }, + /** + * Gets the buckets the user has permissions to write. + * + * @param buckets The buckets object. + */ + filterWritableBuckets: function (buckets) { + var writableBuckets = []; + + buckets.forEach(function (b) { + if (b.permissions.canWrite) { + writableBuckets.push(b); + } + }); + return writableBuckets; + }, + /** * Generates the `autoCompleteBuckets` options for the bucket filter. */ @@ -1213,7 +1330,8 @@ NfRegistryService.parameters = [ TdDataTableService, Router, FdsDialogService, - FdsSnackBarService + FdsSnackBarService, + MatDialog ]; export default NfRegistryService; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js index 455963d3c..9b842fe2c 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js @@ -716,7 +716,7 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }); // The function to test - nfRegistryService.executeDropletAction({name: 'delete'}, { + nfRegistryService.executeDropletAction({name: 'delete data flow'}, { identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', type: 'testTYPE', link: {href: 'testhref'} diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss new file mode 100644 index 000000000..5f1689ff1 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -0,0 +1,162 @@ +/* + * 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. + */ + +$gray: #666; +$light-gray: #999; + +#nifi-registry-import-versioned-flow-dialog, +#nifi-registry-import-new-flow-dialog { + .label-name { + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + color: $gray; + } +} + +.version-count { + font-weight: 500; + padding: 9px 18px; + margin-left: 5px; + border-radius: 17px; + background-color: #eee; + color: $gray; +} + +#new-flow-version-definition, +#new-flow-definition { + max-width: 515px; + border-radius: 2px; +} + +body[fds] input.mat-input-element { + &.file-hover-error { + border: solid 1px #d9150c !important; + background-color: #fff2f2 !important; + } + + &.file-hover-valid { + border: solid 1px #3a870e !important; + } +} + +input[type=text]#new-flow-version-definition-input, +input[type=text]#new-flow-definition-input { + cursor: pointer; +} + +input[type=file]#new-flow-definition { + overflow: hidden; + margin-top: 7px; +} + +#new-data-flow-version-name { + border: none; + height: 34px; + outline: 0; + font-family: Roboto, sans-serif; + max-width: 75%; + font-size: 14px; + color: $gray; + + span { + overflow: hidden; + } +} + +#data-flow-name { + border: none; + height: 34px; + outline: 0; + font-family: Roboto, sans-serif; + font-size: 14px; + color: $gray; + + span { + overflow: hidden; + } +} + +#new-data-flow-version-placeholder { + border: none; + outline: 0; + color: $light-gray; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 300; +} + +#select-flow-file-button, +#select-flow-version-file-button { + position: absolute; + right: 16px; + top: 8px; + border: none; + background-color: transparent; + cursor: pointer; + + i { + color: #6b8791; + padding: 9px 5px; + outline: none; + } + + span { + text-transform: uppercase; + color: #6b8791; + } +} + +input#upload-flow-file-field, +input#upload-versioned-flow-file-field { + display: none; +} + +#new-data-flow-version-comments { + textarea { + width: 515px; + height: 68px; + border: solid 1px #cfd3d7; + border-radius: 2px; + padding: 9px 16px 9px 12px; + resize: none; + color: $gray; + } +} + +#new-data-flow-description { + textarea { + width: 515px; + height: 68px; + border: solid 1px #cfd3d7; + border-radius: 2px; + padding: 9px 16px 9px 12px; + color: $gray; + } +} + +#versioned-flow-file-upload-message-container, +#new-flow-file-upload-message-container { + & span.file-upload-message { + font-size: 12px; + color: $light-gray; + } + + i { + font-size: 15px; + } +} diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss index 37d9ea497..740aa7d22 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss @@ -33,6 +33,7 @@ $fdsFontPath: './node_modules/roboto-fontface/fonts'; @import 'components/administration/users/structureElements'; @import 'components/administration/workflow/structureElements'; @import 'components/explorer/grid-list/structureElements'; +@import 'components/explorer/dialogs/structureElements'; $primaryColor: $rose1; //$green2 $primaryColorHover: $rose2; //$green3 From 36917cd83ea70bf3562815556257c3d5098bc449 Mon Sep 17 00:00:00 2001 From: mtien Date: Tue, 20 Apr 2021 21:09:33 -0700 Subject: [PATCH 02/10] NIFIREG-395 - Address PR feedback. - Revised import REST endpoints that sets a new snapshot metadata to ensure previous Registry data is not used. - Refactored some logic from the export REST endpoint into the ServiceFacade. - Fixed the frontend download API endpoint to use the appropriate href data url. --- .../nifi-registry-web-api/pom.xml | 6 -- .../registry/web/api/BucketFlowResource.java | 69 ++++++++----------- .../registry/web/service/ServiceFacade.java | 2 + .../web/service/StandardServiceFacade.java | 13 ++++ .../main/webapp/services/nf-registry.api.js | 15 ++-- 5 files changed, 49 insertions(+), 56 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml index 444920f79..99cafb4f1 100644 --- a/nifi-registry-core/nifi-registry-web-api/pom.xml +++ b/nifi-registry-core/nifi-registry-web-api/pom.xml @@ -427,12 +427,6 @@ com.fasterxml.jackson.core jackson-databind - - com.google.protobuf - protobuf-java - 3.11.4 - compile - org.apache.nifi.registry diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index a3d542f9f..0bbf97e56 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -27,7 +27,6 @@ import io.swagger.annotations.ExtensionProperty; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.registry.bucket.BucketItem; @@ -63,7 +62,6 @@ import javax.ws.rs.core.Response; import java.util.List; import java.util.SortedSet; -import org.springframework.web.util.UriUtils; @Component @Path("/buckets/{bucketId}/flows") @@ -75,6 +73,7 @@ public class BucketFlowResource extends ApplicationResource { public static final String INVALID_JSON_MESSAGE = "Deserialization of uploaded JSON failed"; + public static final int INITIAL_VERSION = -1; @Autowired public BucketFlowResource(final ServiceFacade serviceFacade, final EventService eventService) { @@ -343,24 +342,22 @@ public Response importVersionedFlow( } // deserialize InputStream to a VersionedFlowSnapshot - VersionedFlowSnapshot versionedFlowSnapshot; - - versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); - - // clear or set the necessary snapShotMetadata - if (versionedFlowSnapshot.getSnapshotMetadata() != null) { - versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); - versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); - versionedFlowSnapshot.getSnapshotMetadata().setLink(null); - versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); - versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); - // if there are new comments, then set it - // otherwise, keep the original comments - if (!StringUtils.isBlank(comments)) { - versionedFlowSnapshot.getSnapshotMetadata().setComments(comments); - } + final VersionedFlowSnapshot versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); + + // set new snapShotMetadata + final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); + metadata.setVersion(INITIAL_VERSION); + + // if there are new comments, then set it + // otherwise, keep the original comments + if (!StringUtils.isBlank(comments)) { + metadata.setComments(comments); + } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && versionedFlowSnapshot.getSnapshotMetadata().getComments() != null) { + metadata.setComments(versionedFlowSnapshot.getSnapshotMetadata().getComments()); } + versionedFlowSnapshot.setSnapshotMetadata(metadata); + return createFlowVersion(bucketId, flowId, versionedFlowSnapshot); } @@ -371,7 +368,7 @@ public Response importVersionedFlow( @ApiOperation( value = "Create flow", notes = "Creates a flow in the given bucket. The flow id is created by the server and populated in the returned entity.", - response = VersionedFlow.class, + response = VersionedFlowSnapshot.class, extensions = { @Extension(name = "access-policy", properties = { @ExtensionProperty(name = "action", value = "write"), @@ -413,17 +410,15 @@ public Response importFlow( // deserialize InputStream and create new VersionedFlowSnapshot final VersionedFlowSnapshot versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); - setSnaphotMetadataIfMissing(bucketId, createdFlow.getIdentifier(), versionedFlowSnapshot); - - // set remaining snapshot metadata - final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); - versionedFlowSnapshot.getSnapshotMetadata().setAuthor(userIdentity); - versionedFlowSnapshot.getSnapshotMetadata().setVersion(-1); + // set new snapshotMetadata + final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); + metadata.setBucketIdentifier(bucketId); + metadata.setFlowIdentifier(createdFlow.getIdentifier()); + metadata.setVersion(INITIAL_VERSION); - final VersionedFlowSnapshot createdSnapshot = serviceFacade.createFlowSnapshot(versionedFlowSnapshot); - publish(EventFactory.flowVersionCreated(createdSnapshot)); + versionedFlowSnapshot.setSnapshotMetadata(metadata); - return Response.status(Response.Status.OK).entity(createdSnapshot).build(); + return createFlowVersion(bucketId, versionedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier(), versionedFlowSnapshot); } @GET @@ -559,23 +554,13 @@ public Response exportVersionedFlow( throw new IllegalArgumentException("The version number is required."); } - final VersionedFlowSnapshot versionedFlowSnapshot = serviceFacade.getFlowSnapshot(bucketId, flowId, versionNumber); - - versionedFlowSnapshot.setFlow(null); - versionedFlowSnapshot.setBucket(null); - versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); - versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); - versionedFlowSnapshot.getSnapshotMetadata().setLink(null); - - String attachmentName = "flow-version-" + versionNumber; - - UriUtils.encodePath(attachmentName, StandardCharsets.UTF_8); - - String filename = attachmentName + ".json"; + final VersionedFlowSnapshot versionedFlowSnapshot = serviceFacade.exportFlowSnapshot(bucketId, flowId, versionNumber); final String versionedFlowSnapshotJsonString = serializeToJson(versionedFlowSnapshot); - return generateOkResponse(versionedFlowSnapshotJsonString).header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filename)).build(); + final String contentDisposition = String.format("attachment; filename=\"flow-version-%d.json\"", versionNumber); + + return generateOkResponse(versionedFlowSnapshotJsonString).header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition).build(); } @GET diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java index b1faac46c..c6c2e087a 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java @@ -102,6 +102,8 @@ public interface ServiceFacade { VersionedFlowSnapshot getLatestFlowSnapshot(String flowIdentifier); + VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber); + SortedSet getFlowSnapshots(String bucketIdentifier, String flowIdentifier); SortedSet getFlowSnapshots(String flowIdentifier); diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java index 85be421f1..f42c897cb 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -360,6 +360,19 @@ public VersionedFlowSnapshot getLatestFlowSnapshot(final String flowIdentifier) return lastSnapshot; } + @Override + public VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber) { + final VersionedFlowSnapshot versionedFlowSnapshot = getFlowSnapshot(bucketIdentifier, flowIdentifier, versionNumber); + + versionedFlowSnapshot.setFlow(null); + versionedFlowSnapshot.setBucket(null); + versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); + versionedFlowSnapshot.getSnapshotMetadata().setLink(null); + + return versionedFlowSnapshot; + } + @Override public SortedSet getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) { authorizeBucketAccess(RequestAction.READ, bucketIdentifier); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index a3f7c3ab8..1bf918186 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -84,15 +84,16 @@ NfRegistryApi.prototype = { */ exportDropletVersionedSnapshot: function (dropletUri, versionNumber) { var self = this; - var url = '../nifi-registry-api/' + dropletUri; - url += '/versions/' + versionNumber + '/export'; + var url = '../nifi-registry-api/' + dropletUri + '/versions/' + versionNumber + '/export'; return this.http.get(url, headers).pipe( map(function (response) { // export the VersionedFlowSnapshot + var data = 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(response, null, 2)); + var element = document.createElement('a'); - element.setAttribute('href', url); - element.setAttribute('download', 'versionedFlow'); + element.setAttribute('href', data); + element.setAttribute('download', 'flow-version-' + response.snapshotMetadata.version); element.style.display = 'none'; document.body.appendChild(element); @@ -124,8 +125,7 @@ NfRegistryApi.prototype = { */ uploadVersionedFlowSnapshot: function (dropletUri, file, comments) { var self = this; - var url = '../nifi-registry-api/' + dropletUri; - url += '/versions/import'; + var url = '../nifi-registry-api/' + dropletUri + '/versions/import'; var formData = new FormData(); formData.append('file', file, 'fileToUpload'); @@ -158,8 +158,7 @@ NfRegistryApi.prototype = { */ uploadFlow: function (bucketUri, file, name, description) { var self = this; - var url = '../nifi-registry-api/' + bucketUri; - url += '/flows/import'; + var url = '../nifi-registry-api/' + bucketUri + '/flows/import'; var formData = new FormData(); formData.append('file', file, 'fileToUpload'); From 78109ae26ce6a134e95632971b8be7114020d219 Mon Sep 17 00:00:00 2001 From: mtien Date: Wed, 21 Apr 2021 21:22:39 -0700 Subject: [PATCH 03/10] NIFIREG-395 - Fix styling in dialogs. --- .../nf-registry-download-version.html | 6 +- .../nf-registry-import-new-flow.html | 12 +- .../nf-registry-import-versioned-flow.html | 22 ++-- .../explorer/dialogs/_structureElements.scss | 106 ++++++++++++------ 4 files changed, 93 insertions(+), 53 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html index 1d24a7354..700915c21 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html @@ -24,11 +24,11 @@ close
-
-
+
+
-
+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html index ae0b1d17f..c682ccfc8 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html @@ -25,7 +25,7 @@ close
-
+
@@ -49,13 +49,13 @@
-
+
-
+
@@ -83,10 +83,9 @@
-
+
Select file
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html index 27f3d3dca..de26b71a4 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html @@ -18,16 +18,16 @@
- + Import New Version
-
+
-
+
@@ -46,7 +46,9 @@
- +
+ +
Looks good! @@ -62,21 +64,21 @@
- + - +
-
- +
+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss index 5f1689ff1..2a3a0ef42 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -17,15 +17,12 @@ $gray: #666; $light-gray: #999; +$dark-gray: #ced3d7; +$teal-gray: #6b8791; -#nifi-registry-import-versioned-flow-dialog, -#nifi-registry-import-new-flow-dialog { - .label-name { - font-family: Roboto, sans-serif; - font-size: 14px; - font-weight: 500; - color: $gray; - } +.label-name { + font-weight: 500; + color: $gray; } .version-count { @@ -37,33 +34,11 @@ $light-gray: #999; color: $gray; } -#new-flow-version-definition, -#new-flow-definition { - max-width: 515px; - border-radius: 2px; -} - -body[fds] input.mat-input-element { - &.file-hover-error { - border: solid 1px #d9150c !important; - background-color: #fff2f2 !important; - } - - &.file-hover-valid { - border: solid 1px #3a870e !important; - } -} - input[type=text]#new-flow-version-definition-input, input[type=text]#new-flow-definition-input { cursor: pointer; } -input[type=file]#new-flow-definition { - overflow: hidden; - margin-top: 7px; -} - #new-data-flow-version-name { border: none; height: 34px; @@ -91,6 +66,10 @@ input[type=file]#new-flow-definition { } } +#new-data-flow-name-input-field { + font-weight: 300; +} + #new-data-flow-version-placeholder { border: none; outline: 0; @@ -110,14 +89,14 @@ input[type=file]#new-flow-definition { cursor: pointer; i { - color: #6b8791; + color: $teal-gray; padding: 9px 5px; outline: none; } span { text-transform: uppercase; - color: #6b8791; + color: $teal-gray; } } @@ -127,9 +106,11 @@ input#upload-versioned-flow-file-field { } #new-data-flow-version-comments { + padding-bottom: 26px; + textarea { width: 515px; - height: 68px; + height: 48px; border: solid 1px #cfd3d7; border-radius: 2px; padding: 9px 16px 9px 12px; @@ -141,14 +122,23 @@ input#upload-versioned-flow-file-field { #new-data-flow-description { textarea { width: 515px; - height: 68px; + height: 48px; border: solid 1px #cfd3d7; border-radius: 2px; padding: 9px 16px 9px 12px; + font-weight: 300; color: $gray; } } +#new-data-flow-version-comments, +#new-data-flow-description { + textarea:focus { + outline: 0; + border-color: $teal-gray; + } +} + #versioned-flow-file-upload-message-container, #new-flow-file-upload-message-container { & span.file-upload-message { @@ -160,3 +150,51 @@ input#upload-versioned-flow-file-field { font-size: 15px; } } + +#nifi-registry-download-versioned-flow-dialog .bucket-dropdown-field .mat-select-value { + color: $gray; +} + +.bucket-dropdown-field .mat-form-field-appearance-fill { + .mat-form-field-infix { + border: 0; + padding: 0.68em 0; + } + + .mat-select-arrow-wrapper { + transform: none; + } + + .mat-form-field-flex { + border: 1px solid $dark-gray; + background-color: transparent; + border-radius: 2px; + padding-top: 0; + } +} + +#new-flow-definition, +#new-flow-version-definition { + mat-form-field { + line-height: 28px; + } + + .mat-form-field-infix { + border-top: 0; + } + + input.mat-input-element { + &.file-hover-error { + border: solid 1px #ef6162; + } + + &.file-hover-valid { + border: solid 1px #2ab377; + } + } +} + +#new-data-flow-container .mat-form-field-appearance-fill .mat-form-field-infix { + font-weight: 300; + color: $light-gray; +} From 38c900fbbfb06f8e237cf4c0870534911487b993 Mon Sep 17 00:00:00 2001 From: mtien Date: Fri, 23 Apr 2021 15:34:56 -0700 Subject: [PATCH 04/10] NIFIREG-395 - Set the initial value for the Bucket dropdown menu in the Import dialogs. - Fixed the flow definition file upload input fields to handle text overflow. NIFIREG-395 - Address PR feedback. - Adjusted import dialog label names. - Updated exported file name and format. - Added messages to inform the user why the Import New Flow button is disabled. --- .../registry/web/api/BucketFlowResource.java | 6 +++- .../nf-registry-import-new-flow.html | 20 ++++++------ .../nf-registry-import-new-flow.js | 23 +++++++++---- .../nf-registry-import-versioned-flow.html | 18 +++++------ .../nf-registry-grid-list-viewer.html | 10 ++++-- .../main/webapp/services/nf-registry.api.js | 5 ++- .../webapp/services/nf-registry.service.js | 13 ++++---- .../explorer/dialogs/_structureElements.scss | 32 ++++++++++--------- .../grid-list/_structureElements.scss | 5 +++ 9 files changed, 82 insertions(+), 50 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 0bbf97e56..de62c504d 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -558,7 +558,11 @@ public Response exportVersionedFlow( final String versionedFlowSnapshotJsonString = serializeToJson(versionedFlowSnapshot); - final String contentDisposition = String.format("attachment; filename=\"flow-version-%d.json\"", versionNumber); + final String flowName = versionedFlowSnapshot.getFlowContents().getName(); + final String dashFlowName = flowName.replaceAll("\\s", "-"); + final String filename = String.format("%s-version-%d.json", dashFlowName, versionedFlowSnapshot.getSnapshotMetadata().getVersion()); + + final String contentDisposition = String.format("attachment; filename=\"%s\"", filename); return generateOkResponse(versionedFlowSnapshotJsonString).header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition).build(); } diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html index c682ccfc8..1138af6e5 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html @@ -25,16 +25,16 @@ close
-
+
- +
-
-
+
+
-
+
@@ -57,8 +57,8 @@
- - + + {{bucket.name}} @@ -123,13 +123,13 @@
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js index b0fcfff66..c82893239 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js @@ -39,12 +39,13 @@ function NfRegistryImportNewFlow(nfRegistryApi, fdsSnackBarService, matDialogRef this.keepDialogOpen = false; this.protocol = location.protocol; this.buckets = data.buckets; + this.activeBucket = data.activeBucket.identifier; this.writableBuckets = []; this.fileToUpload = null; this.fileName = null; this.name = ''; this.description = ''; - this.selectedBucket = ''; + this.selectedBucket = {}; this.hoverValidity = ''; this.extensions = 'application/json'; this.multiple = false; @@ -54,12 +55,18 @@ NfRegistryImportNewFlow.prototype = { constructor: NfRegistryImportNewFlow, ngOnInit: function () { - // only show buckets the user can write - var self = this; - self.writableBuckets = self.filterWritableBuckets(self.buckets); + this.writableBuckets = this.filterWritableBuckets(this.buckets); + + // if there's only 1 writable bucket, always set as the initial value in the bucket dropdown + // if opening the dialog from the explorer/grid-list, there is no active bucket + if (typeof this.activeBucket === 'undefined') { + if (this.writableBuckets.length === 1) { + // set the active bucket + this.activeBucket = this.writableBuckets[0].identifier; + } + } }, - //TODO: get from the service instead filterWritableBuckets: function (buckets) { var self = this; self.writableBuckets = this.writableBuckets; @@ -129,7 +136,11 @@ NfRegistryImportNewFlow.prototype = { var self = this; self.name = this.name; self.description = this.description; - self.selectedBucket = this.selectedBucket; + self.activeBucket = this.activeBucket; + + self.selectedBucket = this.writableBuckets.find(function (b) { + return b.identifier === self.activeBucket; + }); this.nfRegistryApi.uploadFlow(self.selectedBucket.link.href, self.fileToUpload, self.name, self.description).subscribe(function (response) { if (!response.status || response.status === 200) { diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html index de26b71a4..6d9ce90a8 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html @@ -25,15 +25,15 @@ close
-
+
- +
-
-
+
+
-
- +
-
+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html index 294fa56ab..30eb3b20d 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html @@ -38,13 +38,16 @@
+
+ You are not authorized to import flows. +
@@ -126,7 +129,10 @@
-
+

No results match this query.

+
+

There are no buckets to display.

+
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 1bf918186..c8f331901 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -93,7 +93,10 @@ NfRegistryApi.prototype = { var element = document.createElement('a'); element.setAttribute('href', data); - element.setAttribute('download', 'flow-version-' + response.snapshotMetadata.version); + + var flowName = response.flowContents.name; + var dashFlowName = flowName.replaceAll(/\s/g, '-'); + element.setAttribute('download', dashFlowName + '-version-' + response.snapshotMetadata.version); element.style.display = 'none'; document.body.appendChild(element); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js index 099d7b60a..96e862fec 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js @@ -155,7 +155,7 @@ function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, } }, { - name: 'Delete data flow', + name: 'Delete flow', icon: 'fa fa-trash', tooltip: 'Delete', disabled: function (droplet) { @@ -426,7 +426,7 @@ NfRegistryService.prototype = { deleteDroplet: function (droplet) { var self = this; this.dialogService.openConfirm({ - title: 'Delete ' + droplet.type.toLowerCase(), + title: 'Delete Flow', message: 'All versions of this ' + droplet.type.toLowerCase() + ' will be deleted.', cancelButton: 'Cancel', acceptButton: 'Delete', @@ -480,15 +480,16 @@ NfRegistryService.prototype = { * Opens the import new flow dialog. * * @param buckets The buckets object. + * @param activeBucket The active bucket object. */ - openImportNewFlowDialog: function (buckets) { + openImportNewFlowDialog: function (buckets, activeBucket) { var self = this; - this.matDialog.open(NfRegistryImportNewFlow, { disableClose: true, width: '550px', data: { - buckets: buckets + buckets: buckets, + activeBucket: activeBucket } }).afterClosed().subscribe(function (flowUri) { if (flowUri != null) { @@ -532,7 +533,7 @@ NfRegistryService.prototype = { // Opens the download flow version dialog this.openDownloadVersionedFlowDialog(droplet); break; - case 'delete data flow': + case 'delete flow': // Deletes the entire data flow this.deleteDroplet(droplet); break; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss index 2a3a0ef42..90f2a7ebd 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -34,17 +34,11 @@ $teal-gray: #6b8791; color: $gray; } -input[type=text]#new-flow-version-definition-input, -input[type=text]#new-flow-definition-input { - cursor: pointer; -} - -#new-data-flow-version-name { +#flow-version-name { border: none; height: 34px; outline: 0; font-family: Roboto, sans-serif; - max-width: 75%; font-size: 14px; color: $gray; @@ -53,7 +47,7 @@ input[type=text]#new-flow-definition-input { } } -#data-flow-name { +#flow-name { border: none; height: 34px; outline: 0; @@ -66,7 +60,7 @@ input[type=text]#new-flow-definition-input { } } -#new-data-flow-name-input-field { +#new-flow-name-input-field { font-weight: 300; } @@ -105,7 +99,7 @@ input#upload-versioned-flow-file-field { display: none; } -#new-data-flow-version-comments { +#flow-version-comments { padding-bottom: 26px; textarea { @@ -119,7 +113,7 @@ input#upload-versioned-flow-file-field { } } -#new-data-flow-description { +#new-flow-description { textarea { width: 515px; height: 48px; @@ -131,8 +125,8 @@ input#upload-versioned-flow-file-field { } } -#new-data-flow-version-comments, -#new-data-flow-description { +#flow-version-comments, +#new-flow-description { textarea:focus { outline: 0; border-color: $teal-gray; @@ -174,7 +168,7 @@ input#upload-versioned-flow-file-field { } #new-flow-definition, -#new-flow-version-definition { +#flow-version-definition { mat-form-field { line-height: 28px; } @@ -194,7 +188,15 @@ input#upload-versioned-flow-file-field { } } -#new-data-flow-container .mat-form-field-appearance-fill .mat-form-field-infix { +#new-flow-container .mat-form-field-appearance-fill .mat-form-field-infix { font-weight: 300; color: $light-gray; } + +input[type=text]#flow-version-definition-input, +input[type=text]#new-flow-definition-input { + padding-right: 125px; + width: 373px; + text-overflow: ellipsis; + cursor: pointer; +} diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss index 68e26b592..9b4b7ac8b 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss @@ -54,3 +54,8 @@ button.nf-registry-change-log-refresh.mat-icon-button { overflow: auto; margin-bottom: 0; } + +#import-new-flow-disabled-message { + font-size: 12px; + color: #5a656d; +} From e0a5bc748b0d56f3f91c46583554802d20cc1228 Mon Sep 17 00:00:00 2001 From: mtien Date: Wed, 28 Apr 2021 18:49:49 -0700 Subject: [PATCH 05/10] NIFIREG-395 - Handled a bad request when an invalid snapshot file is uploaded. - Refactored client side import file methods to directly pass the file to the server as an API param. - Refactored client side uploadFlow method to re-use BucketFlowResource.createFlow. - Removed BucketFlowResource.importFlow method. - Refactored logic in BucketFlowResource.importVersionedFlow to the service facade. --- .../registry/web/api/BucketFlowResource.java | 123 ++---------------- .../registry/web/service/ServiceFacade.java | 2 + .../web/service/StandardServiceFacade.java | 26 ++++ .../nf-registry-import-new-flow.js | 5 +- .../nf-registry-import-versioned-flow.js | 5 +- .../main/webapp/services/nf-registry.api.js | 55 +++++--- 6 files changed, 81 insertions(+), 135 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index de62c504d..baff2a732 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -26,7 +26,7 @@ import io.swagger.annotations.Extension; import io.swagger.annotations.ExtensionProperty; import java.io.IOException; -import java.io.InputStream; +import javax.ws.rs.HeaderParam; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.registry.bucket.BucketItem; @@ -42,7 +42,6 @@ import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider; import org.apache.nifi.registry.web.service.ServiceFacade; -import org.glassfish.jersey.media.multipart.FormDataParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -72,9 +71,6 @@ ) public class BucketFlowResource extends ApplicationResource { - public static final String INVALID_JSON_MESSAGE = "Deserialization of uploaded JSON failed"; - public static final int INITIAL_VERSION = -1; - @Autowired public BucketFlowResource(final ServiceFacade serviceFacade, final EventService eventService) { super(serviceFacade, eventService); @@ -110,8 +106,8 @@ public Response createFlow( verifyPathParamsMatchBody(bucketId, flow); - final VersionedFlow createdFlow = createAndPublishVersionedFlow(bucketId, flow); - + final VersionedFlow createdFlow = serviceFacade.createFlow(bucketId, flow); + publish(EventFactory.flowCreated(createdFlow)); return Response.status(Response.Status.OK).entity(createdFlow).build(); } @@ -304,7 +300,7 @@ public Response createFlowVersion( @POST @Path("{flowId}/versions/import") - @Consumes(MediaType.MULTIPART_FORM_DATA) + @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( value = "Upload flow version", @@ -330,95 +326,12 @@ public Response importVersionedFlow( @PathParam("flowId") @ApiParam(value = "The flow identifier") final String flowId, - @FormDataParam("file") final InputStream in, - @FormDataParam("comments") final String comments) { - - if (StringUtils.isBlank(bucketId)) { - throw new IllegalArgumentException("The bucket identifier is required."); - } - - if (StringUtils.isBlank(flowId)) { - throw new IllegalArgumentException("The flow identifier is required."); - } - - // deserialize InputStream to a VersionedFlowSnapshot - final VersionedFlowSnapshot versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); - - // set new snapShotMetadata - final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); - metadata.setVersion(INITIAL_VERSION); - - // if there are new comments, then set it - // otherwise, keep the original comments - if (!StringUtils.isBlank(comments)) { - metadata.setComments(comments); - } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && versionedFlowSnapshot.getSnapshotMetadata().getComments() != null) { - metadata.setComments(versionedFlowSnapshot.getSnapshotMetadata().getComments()); - } - - versionedFlowSnapshot.setSnapshotMetadata(metadata); - - return createFlowVersion(bucketId, flowId, versionedFlowSnapshot); - } - - @POST - @Path("import") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Produces(MediaType.APPLICATION_JSON) - @ApiOperation( - value = "Create flow", - notes = "Creates a flow in the given bucket. The flow id is created by the server and populated in the returned entity.", - response = VersionedFlowSnapshot.class, - extensions = { - @Extension(name = "access-policy", properties = { - @ExtensionProperty(name = "action", value = "write"), - @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}")}) - } - ) - @ApiResponses({ - @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), - @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), - @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), - @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), - @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)}) - public Response importFlow( - @PathParam("bucketId") - @ApiParam("The bucket identifier") final String bucketId, - @FormDataParam("file") final InputStream in, - @FormDataParam("name") final String name, - @FormDataParam("description") final String description) { - - if (StringUtils.isBlank(bucketId)) { - throw new IllegalArgumentException("The bucket identifier is required."); - } - - if (StringUtils.isBlank(name)) { - throw new IllegalArgumentException("The flow name is required."); - } - - // create VersionedFlow - final VersionedFlow versionedFlow = new VersionedFlow(); - - versionedFlow.setName(name); - versionedFlow.setRevision(new RevisionInfo(null, 0L)); - if (!StringUtils.isBlank(description)) { - versionedFlow.setDescription(description); - } - - final VersionedFlow createdFlow = createAndPublishVersionedFlow(bucketId, versionedFlow); - - // deserialize InputStream and create new VersionedFlowSnapshot - final VersionedFlowSnapshot versionedFlowSnapshot = deserializeVersionedFlowSnapshot(in); - - // set new snapshotMetadata - final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); - metadata.setBucketIdentifier(bucketId); - metadata.setFlowIdentifier(createdFlow.getIdentifier()); - metadata.setVersion(INITIAL_VERSION); + @ApiParam("file") final VersionedFlowSnapshot versionedFlowSnapshot, + @HeaderParam("comments") final String comments) { - versionedFlowSnapshot.setSnapshotMetadata(metadata); - - return createFlowVersion(bucketId, versionedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier(), versionedFlowSnapshot); + final VersionedFlowSnapshot createdSnapshot = serviceFacade.importVersionedFlowSnapshot(versionedFlowSnapshot, bucketId, flowId, comments); + publish(EventFactory.flowVersionCreated(createdSnapshot)); + return Response.status(Response.Status.CREATED).entity(createdSnapshot).build(); } @GET @@ -729,22 +642,4 @@ private String serializeToJson(final VersionedFlowSnapshot dto) { throw new IllegalArgumentException("Deserialization of selected flow failed", e); } } - - private static VersionedFlowSnapshot deserializeVersionedFlowSnapshot(@NotNull InputStream in) { - try { - return OBJECT_MAPPER.readValue(in, VersionedFlowSnapshot.class); - } catch (IOException e) { - throw new IllegalArgumentException(INVALID_JSON_MESSAGE, e); - } - } - - private VersionedFlow createAndPublishVersionedFlow( - @NotNull String bucketIdentifier, - @NotNull VersionedFlow versionedFlow) { - - final VersionedFlow createdFlow = serviceFacade.createFlow(bucketIdentifier, versionedFlow); - publish(EventFactory.flowCreated(createdFlow)); - - return createdFlow; - } } diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java index c6c2e087a..1788368b0 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java @@ -102,6 +102,8 @@ public interface ServiceFacade { VersionedFlowSnapshot getLatestFlowSnapshot(String flowIdentifier); + VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot, String bucketIdentifier, String flowIdentifier, String comments); + VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber); SortedSet getFlowSnapshots(String bucketIdentifier, String flowIdentifier); diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java index f42c897cb..e3bbfc49f 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -99,6 +99,7 @@ public class StandardServiceFacade implements ServiceFacade { public static final String ACCESS_POLICY_ENTITY_TYPE = "Access Policy"; public static final String VERSIONED_FLOW_ENTITY_TYPE = "Versioned Flow"; public static final String BUCKET_ENTITY_TYPE = "Bucket"; + public static final int INITIAL_VERSION = -1; private final RegistryService registryService; private final ExtensionService extensionService; @@ -360,6 +361,31 @@ public VersionedFlowSnapshot getLatestFlowSnapshot(final String flowIdentifier) return lastSnapshot; } + @Override + public VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot, String bucketIdentifier, + String flowIdentifier, String comments) { + // set new snapshotMetadata + final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); + metadata.setBucketIdentifier(bucketIdentifier); + metadata.setFlowIdentifier(flowIdentifier); + metadata.setVersion(INITIAL_VERSION); + + // if there are new comments, then set it + // otherwise, keep the original comments + if (!StringUtils.isBlank(comments)) { + metadata.setComments(comments); + } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && versionedFlowSnapshot.getSnapshotMetadata().getComments() != null) { + metadata.setComments(versionedFlowSnapshot.getSnapshotMetadata().getComments()); + } + + versionedFlowSnapshot.setSnapshotMetadata(metadata); + + final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + versionedFlowSnapshot.getSnapshotMetadata().setAuthor(userIdentity); + + return createFlowSnapshot(versionedFlowSnapshot); + } + @Override public VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber) { final VersionedFlowSnapshot versionedFlowSnapshot = getFlowSnapshot(bucketIdentifier, flowIdentifier, versionNumber); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js index c82893239..0ebd3ecf8 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js @@ -143,7 +143,7 @@ NfRegistryImportNewFlow.prototype = { }); this.nfRegistryApi.uploadFlow(self.selectedBucket.link.href, self.fileToUpload, self.name, self.description).subscribe(function (response) { - if (!response.status || response.status === 200) { + if (!response.status || response.status === 201) { self.snackBarService.openCoaster({ title: 'Success', message: 'Successfully imported ' + response.flow.name + ' to the ' + response.bucket.name + ' bucket.', @@ -173,8 +173,7 @@ NfRegistryImportNewFlow.prototype = { */ cancel: function () { this.dialogRef.close(); - }, - + } }; NfRegistryImportNewFlow.annotations = [ diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js index a3a1bd8a8..d400e9f5e 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js @@ -106,7 +106,7 @@ NfRegistryImportVersionedFlow.prototype = { var comments = this.comments; this.nfRegistryApi.uploadVersionedFlowSnapshot(this.droplet.link.href, this.fileToUpload, comments).subscribe(function (response) { - if (!response.status || response.status === 200) { + if (!response.status || response.status === 201) { self.snackBarService.openCoaster({ title: 'Success', message: 'Successfully imported version ' + response.snapshotMetadata.version + ' of ' + response.flow.name + '.', @@ -135,8 +135,7 @@ NfRegistryImportVersionedFlow.prototype = { */ cancel: function () { this.dialogRef.close(); - }, - + } }; NfRegistryImportVersionedFlow.annotations = [ diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index c8f331901..00874537f 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -19,7 +19,7 @@ import NfStorage from 'services/nf-storage.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { FdsDialogService } from '@nifi-fds/core'; import { of } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; +import { map, catchError, mergeMap, take } from 'rxjs/operators'; var MILLIS_PER_SECOND = 1000; var headers = new Headers({'Content-Type': 'application/json'}); @@ -129,12 +129,11 @@ NfRegistryApi.prototype = { uploadVersionedFlowSnapshot: function (dropletUri, file, comments) { var self = this; var url = '../nifi-registry-api/' + dropletUri + '/versions/import'; + var versionHeaders = new HttpHeaders() + .set('Content-Type', 'application/json') + .set('comments', comments); - var formData = new FormData(); - formData.append('file', file, 'fileToUpload'); - formData.append('comments', comments); - - return this.http.post(url, formData, headers).pipe( + return self.http.post(url, file, { 'headers': versionHeaders }).pipe( map(function (response) { return response; }), @@ -161,16 +160,42 @@ NfRegistryApi.prototype = { */ uploadFlow: function (bucketUri, file, name, description) { var self = this; - var url = '../nifi-registry-api/' + bucketUri + '/flows/import'; - - var formData = new FormData(); - formData.append('file', file, 'fileToUpload'); - formData.append('name', name); - formData.append('description', description); - return this.http.post(url, formData, headers).pipe( - map(function (response) { - return response; + var url = '../nifi-registry-api/' + bucketUri + '/flows'; + var flow = { 'name': name, 'description': description }; + var versionHeaders = new HttpHeaders() + .set('Content-Type', 'application/json') + .set('comments', ''); + + return this.http.post(url, flow, headers).pipe( + take(1), + // create Flow version 0 + mergeMap(function (response) { + var flowUri = response.link.href; + var importVersionUrl = '../nifi-registry-api/' + flowUri + '/versions/import'; + + // import file as Flow version 1 + return self.http.post(importVersionUrl, file, { 'headers': versionHeaders }).pipe( + take(1), + map(function (snapshot) { + return snapshot; + }), + catchError(function (error) { + // delete flow version 0 + var deleteUri = flowUri + '?versions=0'; + self.deleteDroplet(deleteUri).subscribe(function (response) { + return response; + }); + + self.dialogService.openConfirm({ + title: 'Error', + message: error.error, + acceptButton: 'Ok', + acceptButtonColor: 'fds-warn' + }); + return of(error); + }) + ); }), catchError(function (error) { self.dialogService.openConfirm({ From 6f600e3c9b2073be5319f8e31efb82b286da23ca Mon Sep 17 00:00:00 2001 From: mtien Date: Fri, 30 Apr 2021 17:04:38 -0700 Subject: [PATCH 06/10] NIFIREG-395 - Address PR feedback. - Fixed Import New Version dialog to show the next version being imported. - Added frontend and backend integration tests. - Created ExportedVersionedFlowSnapshot object. - Added test resource file and updated rat configuration to skip the file during contrib-check. - Refactored frontend and server side code. --- .../nifi-registry-web-api/pom.xml | 13 +- .../registry/web/api/BucketFlowResource.java | 43 ++--- .../ExportedVersionedFlowSnapshot.java | 65 +++++++ .../registry/web/service/ServiceFacade.java | 2 +- .../web/service/StandardServiceFacade.java | 13 +- .../apache/nifi/registry/web/api/FlowsIT.java | 170 ++++++++++++++++++ .../test-versioned-flow-snapshot.json | 23 +++ .../nf-registry-download-version.html | 2 +- .../nf-registry-import-new-flow.html | 2 +- .../nf-registry-import-versioned-flow.html | 2 +- .../main/webapp/services/nf-registry.api.js | 39 ++-- .../services/nf-registry.service.spec.js | 134 ++++++++++++-- .../explorer/dialogs/_structureElements.scss | 20 ++- 13 files changed, 445 insertions(+), 83 deletions(-) create mode 100644 nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java create mode 100644 nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml index 99cafb4f1..e4ae19b13 100644 --- a/nifi-registry-core/nifi-registry-web-api/pom.xml +++ b/nifi-registry-core/nifi-registry-web-api/pom.xml @@ -247,6 +247,15 @@ + + org.apache.rat + apache-rat-plugin + + + src/test/resources/test-versioned-flow-snapshot.json + + + @@ -423,10 +432,6 @@ jjwt 0.7.0 - - com.fasterxml.jackson.core - jackson-databind - org.apache.nifi.registry diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index baff2a732..25c80f795 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.registry.web.api; -import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -25,7 +24,6 @@ import io.swagger.annotations.Authorization; import io.swagger.annotations.Extension; import io.swagger.annotations.ExtensionProperty; -import java.io.IOException; import javax.ws.rs.HeaderParam; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; @@ -33,6 +31,7 @@ import org.apache.nifi.registry.diff.VersionedFlowDifference; import org.apache.nifi.registry.event.EventFactory; import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.web.service.ExportedVersionedFlowSnapshot; import org.apache.nifi.registry.flow.VersionedFlow; import org.apache.nifi.registry.flow.VersionedFlowSnapshot; import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; @@ -40,7 +39,6 @@ import org.apache.nifi.registry.revision.web.ClientIdParameter; import org.apache.nifi.registry.revision.web.LongParameter; import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; -import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider; import org.apache.nifi.registry.web.service.ServiceFacade; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -76,8 +74,6 @@ public BucketFlowResource(final ServiceFacade serviceFacade, final EventService super(serviceFacade, eventService); } - private static final ObjectMapper OBJECT_MAPPER = ObjectMapperProvider.getMapper(); - @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -455,29 +451,18 @@ public Response exportVersionedFlow( @PathParam("versionNumber") @ApiParam("The version number") final Integer versionNumber) { - if (StringUtils.isBlank(bucketId)) { - throw new IllegalArgumentException("The bucket identifier is required."); - } - - if (StringUtils.isBlank(flowId)) { - throw new IllegalArgumentException("The flow identifier is required."); - } - - if (versionNumber == null) { - throw new IllegalArgumentException("The version number is required."); - } + final ExportedVersionedFlowSnapshot exportedVersionedFlowSnapshot = serviceFacade.exportFlowSnapshot(bucketId, flowId, versionNumber); - final VersionedFlowSnapshot versionedFlowSnapshot = serviceFacade.exportFlowSnapshot(bucketId, flowId, versionNumber); + final VersionedFlowSnapshot versionedFlowSnapshot = exportedVersionedFlowSnapshot.getVersionedFlowSnapshot(); - final String versionedFlowSnapshotJsonString = serializeToJson(versionedFlowSnapshot); + final String contentDisposition = String.format( + "attachment; filename=\"%s\"", + exportedVersionedFlowSnapshot.getFilename()); - final String flowName = versionedFlowSnapshot.getFlowContents().getName(); - final String dashFlowName = flowName.replaceAll("\\s", "-"); - final String filename = String.format("%s-version-%d.json", dashFlowName, versionedFlowSnapshot.getSnapshotMetadata().getVersion()); - - final String contentDisposition = String.format("attachment; filename=\"%s\"", filename); - - return generateOkResponse(versionedFlowSnapshotJsonString).header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition).build(); + return generateOkResponse(versionedFlowSnapshot) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + .header("Filename", exportedVersionedFlowSnapshot.getFilename()) + .build(); } @GET @@ -634,12 +619,4 @@ private static void setSnaphotMetadataIfMissing( flowSnapshot.setSnapshotMetadata(metadata); } - - private String serializeToJson(final VersionedFlowSnapshot dto) { - try { - return OBJECT_MAPPER.writeValueAsString(dto); - } catch (IOException e) { - throw new IllegalArgumentException("Deserialization of selected flow failed", e); - } - } } diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java new file mode 100644 index 000000000..b81a65bae --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java @@ -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. + */ +package org.apache.nifi.registry.web.service; + +import io.swagger.annotations.ApiModel; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; + +/** + *

+ * Represents a snapshot of a versioned flow and a filename for exporting the flow. A versioned flow may change many times + * over the course of its life. This flow is saved to the registry with information such as its name, a description, + * and each version of the flow. + *

+ * + * @see VersionedFlowSnapshot + */ +@ApiModel +public class ExportedVersionedFlowSnapshot { + + @Valid + @NotNull + private VersionedFlowSnapshot versionedFlowSnapshot; + + @Valid + @NotNull + private String filename; + + public ExportedVersionedFlowSnapshot(final VersionedFlowSnapshot versionedFlowSnapshot, final String filename) { + this.versionedFlowSnapshot = versionedFlowSnapshot; + this.filename = filename; + } + + public void setVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot) { + this.versionedFlowSnapshot = versionedFlowSnapshot; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public VersionedFlowSnapshot getVersionedFlowSnapshot() { + return versionedFlowSnapshot; + } + + public String getFilename() { + return filename; + } +} diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java index 1788368b0..85ac6e747 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java @@ -104,7 +104,7 @@ public interface ServiceFacade { VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot, String bucketIdentifier, String flowIdentifier, String comments); - VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber); + ExportedVersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber); SortedSet getFlowSnapshots(String bucketIdentifier, String flowIdentifier); diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java index e3bbfc49f..9364a2eae 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -99,7 +99,6 @@ public class StandardServiceFacade implements ServiceFacade { public static final String ACCESS_POLICY_ENTITY_TYPE = "Access Policy"; public static final String VERSIONED_FLOW_ENTITY_TYPE = "Versioned Flow"; public static final String BUCKET_ENTITY_TYPE = "Bucket"; - public static final int INITIAL_VERSION = -1; private final RegistryService registryService; private final ExtensionService extensionService; @@ -110,6 +109,8 @@ public class StandardServiceFacade implements ServiceFacade { private final PermissionsService permissionsService; private final LinkService linkService; + private static final int LATEST_VERSION = -1; + @Autowired public StandardServiceFacade(final RegistryService registryService, final ExtensionService extensionService, @@ -368,7 +369,7 @@ public VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot v final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); metadata.setBucketIdentifier(bucketIdentifier); metadata.setFlowIdentifier(flowIdentifier); - metadata.setVersion(INITIAL_VERSION); + metadata.setVersion(LATEST_VERSION); // if there are new comments, then set it // otherwise, keep the original comments @@ -387,16 +388,20 @@ public VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot v } @Override - public VersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber) { + public ExportedVersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber) { final VersionedFlowSnapshot versionedFlowSnapshot = getFlowSnapshot(bucketIdentifier, flowIdentifier, versionNumber); + String flowName = versionedFlowSnapshot.getFlow().getName(); + final String dashFlowName = flowName.replaceAll("\\s", "-"); + final String filename = String.format("%s-version-%d.json", dashFlowName, versionedFlowSnapshot.getSnapshotMetadata().getVersion()); + versionedFlowSnapshot.setFlow(null); versionedFlowSnapshot.setBucket(null); versionedFlowSnapshot.getSnapshotMetadata().setBucketIdentifier(null); versionedFlowSnapshot.getSnapshotMetadata().setFlowIdentifier(null); versionedFlowSnapshot.getSnapshotMetadata().setLink(null); - return versionedFlowSnapshot; + return new ExportedVersionedFlowSnapshot(versionedFlowSnapshot, filename); } @Override diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java index 4471494b9..e50f1a301 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.registry.web.api; +import java.io.File; import org.apache.nifi.registry.bucket.BucketItemType; import org.apache.nifi.registry.flow.VersionedFlow; import org.apache.nifi.registry.flow.VersionedFlowSnapshot; @@ -34,11 +35,13 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual; import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotMetadataEqual; import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotsEqual; import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowsEqual; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"}) @@ -541,4 +544,171 @@ public void testFlowNameUniquePerBucket() throws Exception { } + @Test + public void testImportVersionedFlowSnapshot() { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Create a versionedFlowSnapshot to export + // Given: an empty Bucket "3" (see FlowsIT.sql) with a newly created flow + + final String bucketId = "3"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow for creating snapshots"); + flow.setDescription("This is a randomly named flow created by an integration test for the purpose of holding snapshots."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + final String flowId = createdFlow.getIdentifier(); + + // Create snapshotMetadata + final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata(); + flowSnapshotMetadata.setBucketIdentifier(bucketId); + flowSnapshotMetadata.setFlowIdentifier(flowId); + flowSnapshotMetadata.setComments("This is a snapshot created by an integration test."); + + // Create a VersionedFlowSnapshot + final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot(); + flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); + flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group + flowSnapshot.getFlowContents().setName("Test Flow name"); + flowSnapshot.getSnapshotMetadata().setVersion(-1); + + final VersionedFlowSnapshot createdFlowSnapshot = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .request() + .post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + assertNotNull(createdFlowSnapshot.getFlow()); + + final VersionedFlowSnapshot importedFlowSnapshot = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions/import")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .request() + .post(Entity.entity(createdFlowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + assertNotNull(importedFlowSnapshot); + assertEquals(bucketId, importedFlowSnapshot.getSnapshotMetadata().getBucketIdentifier()); + assertEquals(flowId, importedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier()); + assertEquals(2, importedFlowSnapshot.getSnapshotMetadata().getVersion()); + + // =========== Import a Versioned Flow Snapshot =========== + + // GET the versioned Flow that was just imported + + final VersionedFlowSnapshotMetadata[] versionedFlowSnapshots = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .request().get(VersionedFlowSnapshotMetadata[].class); + assertNotNull(versionedFlowSnapshots); + assertEquals(2, versionedFlowSnapshots.length); + assertFlowSnapshotMetadataEqual(importedFlowSnapshot.getSnapshotMetadata(), versionedFlowSnapshots[0], true); + + // GET the imported versionedFlowSnapshot by link + + final VersionedFlowSnapshot importedFlowSnapshotByLink = client + .target(createURL(versionedFlowSnapshots[0].getLink().getUri().toString())) + .request() + .get(VersionedFlowSnapshot.class); + assertFlowSnapshotsEqual(importedFlowSnapshot, importedFlowSnapshotByLink, true); + + // =========== Import another version =========== + + final File testSnapshotFile = new File("src/test/resources/test-versioned-flow-snapshot.json"); + + // Imported Flow id = 2 + final String importedFlowId = importedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier(); + // Imported Bucket id = 3 + final String importedBucketId = importedFlowSnapshot.getSnapshotMetadata().getBucketIdentifier(); + + WebTarget clientRequestTarget = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions/import")) + .resolveTemplate("bucketId", importedBucketId) + .resolveTemplate("flowId", importedFlowId); + + final VersionedFlowSnapshot nextImportedFlowSnapshot = clientRequestTarget + .request(MediaType.APPLICATION_JSON) + .header("content-type", MediaType.APPLICATION_JSON) + .header("comments", "This is a test version") + .post(Entity.entity(testSnapshotFile, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + assertNotNull(nextImportedFlowSnapshot); + assertBucketsEqual(importedFlowSnapshot.getBucket(), nextImportedFlowSnapshot.getBucket(), true); + assertEquals(importedFlowId, nextImportedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier()); + assertEquals(3, nextImportedFlowSnapshot.getSnapshotMetadata().getVersion()); + } + + @Test + public void testExportVersionedFlowSnapshot() { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Create a versionedFlowSnapshot to export + // Given: an empty Bucket "2" (see FlowsIT.sql) with a newly created flow + + final String bucketId = "2"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow for creating snapshots"); + flow.setDescription("This is a randomly named flow created by an integration test for the purpose of holding snapshots."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + final String flowId = createdFlow.getIdentifier(); + + // Create snapshotMetadata + final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata(); + flowSnapshotMetadata.setBucketIdentifier(bucketId); + flowSnapshotMetadata.setFlowIdentifier(flowId); + flowSnapshotMetadata.setComments("This is a snapshot created by an integration test."); + + // Create a VersionedFlowSnapshot + final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot(); + flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); + flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group + flowSnapshot.getFlowContents().setName("Test Flow name"); + flowSnapshot.getSnapshotMetadata().setVersion(-1); + + final VersionedFlowSnapshot createdFlowSnapshot = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .request() + .post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + assertNotNull(createdFlowSnapshot.getFlow()); + assertEquals(1, createdFlowSnapshot.getFlow().getVersionCount()); + + // Get the version number + final Integer testVersionNumber = createdFlowSnapshot.getSnapshotMetadata().getVersion(); + + // Test the exportVersionedFlow method with the version that was just created + final VersionedFlowSnapshot exportedVersionedFlowSnapshot = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions/{versionNumber: \\d+}/export")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .resolveTemplate("versionNumber", testVersionNumber) + .request() + .get(VersionedFlowSnapshot.class); + + assertNotNull(exportedVersionedFlowSnapshot); + assertEquals(createdFlowSnapshot.getSnapshotMetadata().getVersion(), + exportedVersionedFlowSnapshot.getSnapshotMetadata().getVersion()); + assertNull(exportedVersionedFlowSnapshot.getBucket()); + assertNull(exportedVersionedFlowSnapshot.getFlow()); + assertNull(exportedVersionedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier()); + assertNull(exportedVersionedFlowSnapshot.getSnapshotMetadata().getBucketIdentifier()); + assertNull(exportedVersionedFlowSnapshot.getSnapshotMetadata().getLink()); + } } diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json b/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json new file mode 100644 index 000000000..647ca5d5b --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json @@ -0,0 +1,23 @@ +{ + "flowContents": { + "componentType": "PROCESS_GROUP", + "connections": [], + "controllerServices": [], + "funnels": [], + "identifier": "123", + "inputPorts": [], + "labels": [], + "name": "Test snapshot", + "outputPorts": [], + "processGroups": [], + "processors": [], + "remoteProcessGroups": [], + "variables": {} + }, + "snapshotMetadata": { + "author": "anonymous", + "comments": "This is snapshot #5", + "timestamp": 1618078687616, + "version": 5 + } +} \ No newline at end of file diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html index 700915c21..b276884ce 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html @@ -30,7 +30,7 @@
- + Latest version (v.{{snapshotMeta.version}}) Version {{snapshotMeta.version}} diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html index 1138af6e5..9bc0b516e 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.html @@ -57,7 +57,7 @@
- + {{bucket.name}} diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html index 6d9ce90a8..e89437b24 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html @@ -41,7 +41,7 @@ value="{{droplet.name}}"/>
- v.{{droplet.versionCount + 1}} + v.{{droplet.snapshotMetadata.length + 1}}
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 00874537f..8c4eec05a 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -19,7 +19,7 @@ import NfStorage from 'services/nf-storage.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { FdsDialogService } from '@nifi-fds/core'; import { of } from 'rxjs'; -import { map, catchError, mergeMap, take } from 'rxjs/operators'; +import { map, catchError, take, switchMap } from 'rxjs/operators'; var MILLIS_PER_SECOND = 1000; var headers = new Headers({'Content-Type': 'application/json'}); @@ -85,24 +85,25 @@ NfRegistryApi.prototype = { exportDropletVersionedSnapshot: function (dropletUri, versionNumber) { var self = this; var url = '../nifi-registry-api/' + dropletUri + '/versions/' + versionNumber + '/export'; + var options = { + headers: headers, + observe: 'response' + }; - return this.http.get(url, headers).pipe( + return this.http.get(url, options).pipe( map(function (response) { - // export the VersionedFlowSnapshot - var data = 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(response, null, 2)); - - var element = document.createElement('a'); - element.setAttribute('href', data); + // export the VersionedFlowSnapshot by creating a hidden anchor element + var stringSnapshot = encodeURIComponent(JSON.stringify(response.body, null, 2)); + var filename = response.headers.get('Filename'); - var flowName = response.flowContents.name; - var dashFlowName = flowName.replaceAll(/\s/g, '-'); - element.setAttribute('download', dashFlowName + '-version-' + response.snapshotMetadata.version); + var anchorElement = document.createElement('a'); + anchorElement.href = 'data:application/json;charset=utf-8,' + stringSnapshot; + anchorElement.download = filename; + anchorElement.style = 'display: none;'; - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - document.body.removeChild(element); + document.body.appendChild(anchorElement); + anchorElement.click(); + document.body.removeChild(anchorElement); return response; }), @@ -163,20 +164,16 @@ NfRegistryApi.prototype = { var url = '../nifi-registry-api/' + bucketUri + '/flows'; var flow = { 'name': name, 'description': description }; - var versionHeaders = new HttpHeaders() - .set('Content-Type', 'application/json') - .set('comments', ''); return this.http.post(url, flow, headers).pipe( take(1), // create Flow version 0 - mergeMap(function (response) { + switchMap(function (response) { var flowUri = response.link.href; var importVersionUrl = '../nifi-registry-api/' + flowUri + '/versions/import'; // import file as Flow version 1 - return self.http.post(importVersionUrl, file, { 'headers': versionHeaders }).pipe( - take(1), + return self.http.post(importVersionUrl, file, headers).pipe( map(function (snapshot) { return snapshot; }), diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js index 9b842fe2c..3af13220a 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js @@ -22,6 +22,12 @@ import NfRegistryApi from 'services/nf-registry.api'; import NfRegistryService from 'services/nf-registry.service'; import { Router } from '@angular/router'; import { FdsDialogService } from '@nifi-fds/core'; +import NfRegistryDownloadVersionedFlow + from '../components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; +import NfRegistryImportVersionedFlow + from '../components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; +import NfRegistryImportNewFlow + from '../components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; describe('NfRegistry Service isolated unit tests', function () { @@ -716,7 +722,7 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }); // The function to test - nfRegistryService.executeDropletAction({name: 'delete data flow'}, { + nfRegistryService.executeDropletAction({name: 'delete flow'}, { identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', type: 'testTYPE', link: {href: 'testhref'} @@ -726,7 +732,7 @@ describe('NfRegistry Service w/ Angular testing utils', function () { expect(nfRegistryService.droplets.length).toBe(0); expect(nfRegistryService.filterDroplets).toHaveBeenCalled(); const openConfirmCall = nfRegistryService.dialogService.openConfirm.calls.first(); - expect(openConfirmCall.args[0].title).toBe('Delete testtype'); + expect(openConfirmCall.args[0].title).toBe('Delete Flow'); const deleteDropletCall = nfRegistryApi.deleteDroplet.calls.first(); expect(deleteDropletCall.args[0]).toBe('testhref'); }); @@ -795,10 +801,10 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }).and.returnValue(of({identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null})); // object to be updated by the test - const bucket = {identifier: '999', revision: { version: 0}}; + const bucket = {identifier: '999', revision: {version: 0}}; // set up the bucket to be deleted - nfRegistryService.buckets = [bucket, {identifier: 1, revision: { version: 0}}]; + nfRegistryService.buckets = [bucket, {identifier: 1, revision: {version: 0}}]; // The function to test nfRegistryService.executeBucketAction({name: 'delete'}, bucket); @@ -850,7 +856,7 @@ describe('NfRegistry Service w/ Angular testing utils', function () { const user = {identifier: '999', revision: {version: 0}}; // set up the user to be deleted - nfRegistryService.users = [user, {identifier: 1, revision: { version: 0}}]; + nfRegistryService.users = [user, {identifier: 1, revision: {version: 0}}]; // The function to test nfRegistryService.executeUserAction({name: 'delete'}, user); @@ -1023,10 +1029,10 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }).and.returnValue(of({identifier: 999, link: null})); // object to be updated by the test - const bucket = {identifier: 999, checked: true, revision: { version: 0}}; + const bucket = {identifier: 999, checked: true, revision: {version: 0}}; // set up the bucket to be deleted - nfRegistryService.buckets = [bucket, {identifier: 1, revision: { version: 0}}]; + nfRegistryService.buckets = [bucket, {identifier: 1, revision: {version: 0}}]; nfRegistryService.filteredBuckets = nfRegistryService.buckets; // The function to test @@ -1065,13 +1071,13 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }).and.returnValue(of({identifier: 99, link: null})); // object to be updated by the test - const group = {identifier: 999, checked: true, revision: { version: 0}}; - const user = {identifier: 999, checked: true, revision: { version: 0}}; + const group = {identifier: 999, checked: true, revision: {version: 0}}; + const user = {identifier: 999, checked: true, revision: {version: 0}}; // set up the group to be deleted - nfRegistryService.groups = [group, {identifier: 1, revision: { version: 0}}]; + nfRegistryService.groups = [group, {identifier: 1, revision: {version: 0}}]; nfRegistryService.filteredUserGroups = nfRegistryService.groups; - nfRegistryService.users = [user, {identifier: 12, revision: { version: 0}}]; + nfRegistryService.users = [user, {identifier: 12, revision: {version: 0}}]; nfRegistryService.filteredUsers = nfRegistryService.users; // The function to test @@ -1091,4 +1097,110 @@ describe('NfRegistry Service w/ Angular testing utils', function () { expect(nfRegistryService.users.length).toBe(1); expect(nfRegistryService.users[0].identifier).toBe(12); }); + + it('should open the Download Version dialog.', function () { + //Setup the nfRegistryService state for this test + var droplet = { + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + type: 'testTYPE', + link: {href: 'testhref'} + }; + + //Spy + spyOn(nfRegistryService.matDialog, 'open'); + + nfRegistryService.executeDropletAction({name: 'download version'}, droplet); + expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryDownloadVersionedFlow, { + disableClose: true, + width: '400px', + data: { + droplet: droplet + } + }); + }); + + it('should open the Import Versioned Flow dialog.', function () { + //Setup the nfRegistryService state for this test + var droplet = { + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + type: 'testTYPE', + link: {href: 'testhref'} + }; + + //Spy + spyOn(nfRegistryService.matDialog, 'open').and.returnValue({ + afterClosed: function () { + return of(true); + } + }); + + nfRegistryService.executeDropletAction({name: 'import new version'}, droplet); + expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryImportVersionedFlow, { + disableClose: true, + width: '550px', + data: { + droplet: droplet + } + }); + }); + + it('should open the Import New Flow dialog.', function () { + //Setup the nfRegistryService state for this test + nfRegistryService.buckets = [{ + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + name: 'Bucket #1', + checked: true, + permissions: {canDelete: true, canRead: true, canWrite: false} + }, { + identifier: '5c04b4fb-9513-47bb-aa74-1ae34616bfdc', + name: 'Bucket #2', + checked: true, + permissions: {canDelete: true, canRead: true, canWrite: true} + }]; + + nfRegistryService.bucket = { + identifier: '5c04b4fb-9513-47bb-aa74-1ae34616bfdc', + name: 'Bucket #2', + checked: true, + permissions: {canDelete: true, canRead: true, canWrite: true} + }; + + //Spy + spyOn(nfRegistryService.matDialog, 'open').and.returnValue({ + afterClosed: function () { + return of(true); + } + }); + + nfRegistryService.openImportNewFlowDialog(nfRegistryService.buckets, nfRegistryService.bucket); + expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryImportNewFlow, { + disableClose: true, + width: '550px', + data: { + buckets: nfRegistryService.buckets, + activeBucket: nfRegistryService.bucket + } + }); + }); + + it('should filter writable buckets.', function () { + nfRegistryService.buckets = [{ + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + name: 'Bucket #1', + checked: true, + permissions: {canDelete: true, canRead: true, canWrite: false} + }, { + identifier: '5c04b4fb-9513-47bb-aa74-1ae34616bfdc', + name: 'Bucket #2', + checked: true, + permissions: {canDelete: true, canRead: true, canWrite: true} + }]; + + // The function to test + const writableBuckets = nfRegistryService.filterWritableBuckets(nfRegistryService.buckets); + + // assertions + expect(writableBuckets.length).toBe(1); + expect(writableBuckets[0].name).toBe('Bucket #2'); + }); }); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss index 90f2a7ebd..4a4b5b1e9 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -60,10 +60,6 @@ $teal-gray: #6b8791; } } -#new-flow-name-input-field { - font-weight: 300; -} - #new-data-flow-version-placeholder { border: none; outline: 0; @@ -120,7 +116,6 @@ input#upload-versioned-flow-file-field { border: solid 1px #cfd3d7; border-radius: 2px; padding: 9px 16px 9px 12px; - font-weight: 300; color: $gray; } } @@ -153,6 +148,14 @@ input#upload-versioned-flow-file-field { .mat-form-field-infix { border: 0; padding: 0.68em 0; + + .mat-select-value-text { + color: $gray; + } + + .mat-select-placeholder { + font-weight: 300; + } } .mat-select-arrow-wrapper { @@ -167,6 +170,12 @@ input#upload-versioned-flow-file-field { } } +.bucket-dropdown-select { + .mat-select-panel { + color: $gray; + } +} + #new-flow-definition, #flow-version-definition { mat-form-field { @@ -189,7 +198,6 @@ input#upload-versioned-flow-file-field { } #new-flow-container .mat-form-field-appearance-fill .mat-form-field-infix { - font-weight: 300; color: $light-gray; } From bdb4d06c8a7f5a30827fdc380488d20a29cede88 Mon Sep 17 00:00:00 2001 From: mtien Date: Wed, 12 May 2021 13:14:07 -0700 Subject: [PATCH 07/10] NIFIREG-395 - Address PR feedback. - Updated Flow Actions menu hover and focus styles. - Changed dialog title to 'Export Version', the latest menu item name, and renamed the component. - Disabled frontend export flows depending on read permissions. - Added and updated tests --- .../registry/web/api/BucketFlowResource.java | 6 +- .../web/service/StandardServiceFacade.java | 4 +- .../apache/nifi/registry/web/api/FlowsIT.java | 6 +- .../nf-registry-export-version.html} | 14 +- .../nf-registry-export-versioned-flow.js} | 22 +-- .../nf-registry-export-versioned-flow.spec.js | 141 +++++++++++++++ .../nf-registry-import-new-flow.js | 3 +- .../nf-registry-import-new-flow.spec.js | 96 +++++++++++ .../nf-registry-import-versioned-flow.html | 3 +- .../nf-registry-import-versioned-flow.js | 1 - .../nf-registry-import-versioned-flow.spec.js | 87 ++++++++++ ...f-registry-bucket-grid-list-viewer.spec.js | 18 +- ...-registry-droplet-grid-list-viewer.spec.js | 24 ++- .../nf-registry-grid-list-viewer.spec.js | 9 +- .../src/main/webapp/nf-registry.module.js | 6 +- .../main/webapp/services/nf-registry.api.js | 10 +- .../webapp/services/nf-registry.api.spec.js | 162 ++++++++++++++++++ .../webapp/services/nf-registry.service.js | 20 +-- .../services/nf-registry.service.spec.js | 26 ++- .../explorer/dialogs/_structureElements.scss | 12 +- 20 files changed, 595 insertions(+), 75 deletions(-) rename nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/{download-versioned-flow/nf-registry-download-version.html => export-versioned-flow/nf-registry-export-version.html} (78%) rename nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/{download-versioned-flow/nf-registry-download-versioned-flow.js => export-versioned-flow/nf-registry-export-versioned-flow.js} (81%) create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.spec.js create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.spec.js create mode 100644 nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.spec.js diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 25c80f795..067b3078c 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -299,8 +299,8 @@ public Response createFlowVersion( @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ApiOperation( - value = "Upload flow version", - notes = "Uploads the next version of a flow. The version number of the object being created must be the " + + value = "Import flow version", + notes = "Import the next version of a flow. The version number of the object being created will be the " + "next available version integer. Flow versions are immutable after they are created.", response = VersionedFlowSnapshot.class, extensions = { @@ -323,7 +323,7 @@ public Response importVersionedFlow( @ApiParam(value = "The flow identifier") final String flowId, @ApiParam("file") final VersionedFlowSnapshot versionedFlowSnapshot, - @HeaderParam("comments") final String comments) { + @HeaderParam("Comments") final String comments) { final VersionedFlowSnapshot createdSnapshot = serviceFacade.importVersionedFlowSnapshot(versionedFlowSnapshot, bucketId, flowId, comments); publish(EventFactory.flowVersionCreated(createdSnapshot)); diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java index 9364a2eae..dec1a87f7 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -373,9 +373,9 @@ public VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot v // if there are new comments, then set it // otherwise, keep the original comments - if (!StringUtils.isBlank(comments)) { + if (StringUtils.isNotBlank(comments)) { metadata.setComments(comments); - } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && versionedFlowSnapshot.getSnapshotMetadata().getComments() != null) { + } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && StringUtils.isNotBlank(versionedFlowSnapshot.getSnapshotMetadata().getComments())) { metadata.setComments(versionedFlowSnapshot.getSnapshotMetadata().getComments()); } diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java index e50f1a301..b04f87216 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java +++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java @@ -47,6 +47,8 @@ @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"}) public class FlowsIT extends UnsecuredITBase { + private static final int LATEST_VERSION = -1; + @Test public void testGetFlowsEmpty() throws Exception { @@ -576,7 +578,7 @@ public void testImportVersionedFlowSnapshot() { flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group flowSnapshot.getFlowContents().setName("Test Flow name"); - flowSnapshot.getSnapshotMetadata().setVersion(-1); + flowSnapshot.getSnapshotMetadata().setVersion(LATEST_VERSION); final VersionedFlowSnapshot createdFlowSnapshot = client .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) @@ -678,7 +680,7 @@ public void testExportVersionedFlowSnapshot() { flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group flowSnapshot.getFlowContents().setName("Test Flow name"); - flowSnapshot.getSnapshotMetadata().setVersion(-1); + flowSnapshot.getSnapshotMetadata().setVersion(LATEST_VERSION); final VersionedFlowSnapshot createdFlowSnapshot = client .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html similarity index 78% rename from nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html rename to nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html index b276884ce..4ac01e9e4 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-version.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html @@ -15,10 +15,10 @@ limitations under the License. --> -
+
- Download Version + Export Version -
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js similarity index 81% rename from nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js rename to nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js index b686ca9db..beb40c5a5 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js @@ -21,7 +21,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; import { FdsSnackBarService } from '@nifi-fds/core'; /** - * NfRegistryDownloadVersionedFlow constructor. + * NfRegistryExportVersionedFlow constructor. * * @param nfRegistryApi The api service. * @param fdsSnackBarService The FDS snack bar service module. @@ -29,7 +29,7 @@ import { FdsSnackBarService } from '@nifi-fds/core'; * @param data The data passed into this component. * @constructor */ -function NfRegistryDownloadVersionedFlow(nfRegistryApi, fdsSnackBarService, matDialogRef, data) { +function NfRegistryExportVersionedFlow(nfRegistryApi, fdsSnackBarService, matDialogRef, data) { // Services this.snackBarService = fdsSnackBarService; this.nfRegistryApi = nfRegistryApi; @@ -41,13 +41,13 @@ function NfRegistryDownloadVersionedFlow(nfRegistryApi, fdsSnackBarService, matD this.selectedVersion = this.droplet.snapshotMetadata[0].version; } -NfRegistryDownloadVersionedFlow.prototype = { - constructor: NfRegistryDownloadVersionedFlow, +NfRegistryExportVersionedFlow.prototype = { + constructor: NfRegistryExportVersionedFlow, /** - * Download specified versioned flow snapshot. + * Export specified versioned flow snapshot. */ - downloadVersion: function () { + exportVersion: function () { var self = this; var version = this.selectedVersion; @@ -73,24 +73,24 @@ NfRegistryDownloadVersionedFlow.prototype = { }, /** - * Cancel creation of a download version and close dialog. + * Cancel an export of a version and close dialog. */ cancel: function () { this.dialogRef.close(); } }; -NfRegistryDownloadVersionedFlow.annotations = [ +NfRegistryExportVersionedFlow.annotations = [ new Component({ - templateUrl: './nf-registry-download-version.html' + templateUrl: './nf-registry-export-version.html' }) ]; -NfRegistryDownloadVersionedFlow.parameters = [ +NfRegistryExportVersionedFlow.parameters = [ NfRegistryApi, FdsSnackBarService, MatDialogRef, MAT_DIALOG_DATA ]; -export default NfRegistryDownloadVersionedFlow; +export default NfRegistryExportVersionedFlow; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.spec.js new file mode 100644 index 000000000..6d49ec743 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.spec.js @@ -0,0 +1,141 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import { of } from 'rxjs'; + +import NfRegistryExportVersionedFlow from 'components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow'; + +describe('NfRegistryExportVersionedFlow Component unit tests', function () { + var nfRegistryApi; + var comp; + + beforeEach(function () { + nfRegistryApi = new NfRegistryApi(); + + var data = { + droplet: { + bucketIdentifier: '123', + bucketName: 'Bucket 1', + createdTimestamp: 1620177743648, + description: '', + identifier: '555', + link: { + href: 'buckets/123/flows/555', + params: {} + }, + modifiedTimestamp: 1620177743687, + name: 'Test Flow', + permissions: {canDelete: true, canRead: true, canWrite: true}, + revision: {version: 0}, + snapshotMetadata: [ + { + author: 'anonymous', + bucketIdentifier: '123', + comments: 'Test comments', + flowIdentifier: '555', + link: { + href: 'buckets/123/flows/555/versions/1', + params: {} + } + }, + { + author: 'anonymous', + bucketIdentifier: '123', + comments: 'Test comments', + flowIdentifier: '999', + link: { + href: 'buckets/123/flows/999/versions/2', + params: {} + } + } + ], + type: 'Flow', + versionCount: 2 + } + }; + + comp = new NfRegistryExportVersionedFlow(nfRegistryApi, { openCoaster: function () {} }, { close: function () {} }, data); + + var response = { + body: { + flowContents: { + componentType: 'PROCESS_GROUP', + connections: [], + controllerServices: [], + funnels: [], + identifier: '555', + inputPorts: [], + labels: [], + name: 'Test snapshot', + outputPorts: [], + processGroups: [] + }, + snapshotMetadata: { + author: 'anonymous', + bucketIdentifier: '123', + comments: 'Test comments', + flowIdentifier: '555', + link: { + href: 'buckets/123/flows/555/versions/2', + params: {} + }, + version: 2 + } + }, + headers: { + headers: [ + {'filename': ['Test-flow-version-1']} + ] + }, + ok: true, + status: 200, + statusText: 'OK', + type: 4, + url: 'testUrl' + }; + + // Spy + spyOn(nfRegistryApi, 'exportDropletVersionedSnapshot').and.callFake(function () { + }).and.returnValue(of(response)); + spyOn(comp.dialogRef, 'close'); + }); + + it('should create component', function () { + expect(comp).toBeDefined(); + }); + + it('should export a versioned flow snapshot and close the dialog', function () { + spyOn(comp, 'exportVersion').and.callThrough(); + + // The function to test + comp.exportVersion(); + + //assertions + expect(comp).toBeDefined(); + expect(comp.exportVersion).toHaveBeenCalled(); + expect(comp.dialogRef.close).toHaveBeenCalled(); + }); + + it('should cancel the export of a flow snapshot', function () { + // the function to test + comp.cancel(); + + //assertions + expect(comp.dialogRef.close).toHaveBeenCalled(); + }); +}); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js index 0ebd3ecf8..978a79667 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.js @@ -37,7 +37,6 @@ function NfRegistryImportNewFlow(nfRegistryApi, fdsSnackBarService, matDialogRef this.dialogRef = matDialogRef; // local state this.keepDialogOpen = false; - this.protocol = location.protocol; this.buckets = data.buckets; this.activeBucket = data.activeBucket.identifier; this.writableBuckets = []; @@ -59,7 +58,7 @@ NfRegistryImportNewFlow.prototype = { // if there's only 1 writable bucket, always set as the initial value in the bucket dropdown // if opening the dialog from the explorer/grid-list, there is no active bucket - if (typeof this.activeBucket === 'undefined') { + if (this.activeBucket === undefined) { if (this.writableBuckets.length === 1) { // set the active bucket this.activeBucket = this.writableBuckets[0].identifier; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.spec.js new file mode 100644 index 000000000..b315a28dd --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow.spec.js @@ -0,0 +1,96 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import NfRegistryImportNewFlow from 'components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; + +describe('NfRegistryImportNewFlow Component unit tests', function () { + var nfRegistryApi; + var data; + var comp; + var testFile; + + beforeEach(function () { + nfRegistryApi = new NfRegistryApi(); + testFile = new File([], 'filename.json'); + data = { + activeBucket: {}, + buckets: [ + { + allowBundleRedeploy: false, + allowPublicRead: false, + createdTimestamp: 1620168925108, + identifier: '123', + link: { + href: 'buckets/123', + params: {} + }, + name: 'Bucket 1', + permissions: {canDelete: true, canRead: true, canWrite: true}, + revision: {version: 0} + }, + { + allowBundleRedeploy: false, + allowPublicRead: false, + createdTimestamp: 1620168925108, + identifier: '456', + link: { + href: 'buckets/456', + params: {} + }, + name: 'Bucket 2', + permissions: {canDelete: true, canRead: true, canWrite: false}, + revision: {version: 0} + }, + ] + }; + + comp = new NfRegistryImportNewFlow(nfRegistryApi, { openCoaster: function () {} }, { close: function () {} }, data); + + //Spy + spyOn(comp.dialogRef, 'close'); + }); + + it('should create component', function () { + expect(comp).toBeDefined(); + }); + + it('should cancel the import of a new version', function () { + // the function to test + comp.cancel(); + + //assertions + expect(comp.dialogRef.close).toHaveBeenCalled(); + }); + + it('should assign writable and active buckets on init', function () { + comp.ngOnInit(); + expect(comp.writableBuckets.length).toBe(1); + expect(comp.activeBucket).toBe(data.buckets[0].identifier); + }); + + it('should handle file input', function () { + var jsonFilename = 'filename.json'; + + // The function to test + comp.handleFileInput([testFile]); + + //assertions + expect(comp.fileToUpload.name).toEqual(jsonFilename); + expect(comp.fileName).toEqual('filename'); + }); +}); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html index e89437b24..4d7201b83 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.html @@ -82,8 +82,7 @@ autocomplete="off" [ngClass]="{'file-hover-valid': (hoverValidity === 'valid'), 'file-hover-error': (hoverValidity === 'invalid'), - 'file-selected': (fileToUpload != null), 'multiple': multiple}" - style="white-space: nowrap;overflow: hidden;text-overflow: ellipsis;"/> + 'file-selected': (fileToUpload != null), 'multiple': multiple}"/>
Select file diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js index d400e9f5e..a41c89c92 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.js @@ -36,7 +36,6 @@ function NfRegistryImportVersionedFlow(nfRegistryApi, fdsSnackBarService, matDia this.dialogRef = matDialogRef; // local state this.keepDialogOpen = false; - this.protocol = location.protocol; this.droplet = data.droplet; this.fileToUpload = null; this.fileName = null; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.spec.js new file mode 100644 index 000000000..ae3b50457 --- /dev/null +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow.spec.js @@ -0,0 +1,87 @@ +/* + * 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 NfRegistryApi from 'services/nf-registry.api'; +import NfRegistryImportVersionedFlow from 'components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; + +describe('NfRegistryImportVersionedFlow Component unit tests', function () { + var nfRegistryApi; + var data; + var comp; + + beforeEach(function () { + nfRegistryApi = new NfRegistryApi(); + + data = { + droplet: { + bucketIdentifier: '123', + bucketName: 'Bucket 2', + createdTimestamp: 1620177743648, + description: '', + identifier: '555', + link: { + href: 'buckets/123/flows/555', + params: {} + }, + modifiedTimestamp: 1620177743687, + name: 'Test Flow 2', + permissions: {canDelete: true, canRead: true, canWrite: true}, + revision: {version: 0}, + snapshotMetadata: [ + { + author: 'anonymous', + bucketIdentifier: '123', + comments: 'Test comments', + flowIdentifier: '555', + link: {} + } + ], + type: 'Flow', + versionCount: 1 + } + }; + + comp = new NfRegistryImportVersionedFlow(nfRegistryApi, { openCoaster: function () {} }, { close: function () {} }, data); + + // Spy + spyOn(comp.dialogRef, 'close'); + }); + + it('should create component', function () { + expect(comp).toBeDefined(); + }); + + it('should cancel the import of a new version', function () { + // the function to test + comp.cancel(); + + //assertions + expect(comp.dialogRef.close).toHaveBeenCalled(); + }); + + it('should handle file input', function () { + var jsonFilename = 'filename.json'; + var testFile = new File([], jsonFilename); + + // The function to test + comp.handleFileInput([testFile]); + + //assertions + expect(comp.fileToUpload.name).toEqual(jsonFilename); + expect(comp.fileName).toEqual('filename'); + }); +}); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.spec.js index 21b5553de..0f47ac166 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.spec.js @@ -67,12 +67,14 @@ describe('NfRegistryBucketGridListViewer Component', function () { spyOn(nfRegistryApi, 'getBuckets').and.callFake(function () { }).and.returnValue(of([{ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); spyOn(nfRegistryApi, 'getBucket').and.callFake(function () { }).and.returnValue(of({ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getDroplets').and.callFake(function () { }).and.returnValue(of([{ @@ -89,7 +91,8 @@ describe('NfRegistryBucketGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets, getBucket, and getDroplets calls fixture.detectChanges(); @@ -150,12 +153,14 @@ describe('NfRegistryBucketGridListViewer Component', function () { spyOn(nfRegistryApi, 'getBuckets').and.callFake(function () { }).and.returnValue(of([{ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); spyOn(nfRegistryApi, 'getBucket').and.callFake(function () { }).and.returnValue(of({ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getDroplets').and.callFake(function () { }).and.returnValue(of([{ @@ -172,7 +177,8 @@ describe('NfRegistryBucketGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets, getBucket, and getDroplets calls fixture.detectChanges(); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.spec.js index 40d929322..6965762f5 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.spec.js @@ -84,17 +84,20 @@ describe('NfRegistryDropletGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getBuckets').and.callFake(function () { }).and.returnValue(of([{ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); spyOn(nfRegistryApi, 'getBucket').and.callFake(function () { }).and.returnValue(of({ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getDroplets').and.callFake(function () { }).and.returnValue(of([{ @@ -111,7 +114,8 @@ describe('NfRegistryDropletGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets, getBucket, getDroplet, and getDroplets calls fixture.detectChanges(); @@ -194,17 +198,20 @@ describe('NfRegistryDropletGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getBuckets').and.callFake(function () { }).and.returnValue(of([{ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); spyOn(nfRegistryApi, 'getBucket').and.callFake(function () { }).and.returnValue(of({ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} })); spyOn(nfRegistryApi, 'getDroplets').and.callFake(function () { }).and.returnValue(of([{ @@ -221,7 +228,8 @@ describe('NfRegistryDropletGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets, getBucket, getDroplet, and getDroplets calls fixture.detectChanges(); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.spec.js index 117f635ae..381327ad3 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.spec.js @@ -60,7 +60,8 @@ describe('NfRegistryGridListViewer Component', function () { spyOn(nfRegistryApi, 'getBuckets').and.callFake(function () { }).and.returnValue(of([{ identifier: '2f7f9e54-dc09-4ceb-aa58-9fe581319cdc', - name: 'Bucket #1' + name: 'Bucket #1', + permissions: {'canDelete': true, 'canRead': true, 'canWrite': true} }])); spyOn(nfRegistryService, 'filterDroplets'); @@ -84,7 +85,8 @@ describe('NfRegistryGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets and getDroplets calls fixture.detectChanges(); @@ -125,7 +127,8 @@ describe('NfRegistryGridListViewer Component', function () { 'rel': 'self' }, 'href': 'flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc' - } + }, + 'permissions': {'canDelete': true, 'canRead': true, 'canWrite': true} }])); // 1st change detection triggers ngOnInit which makes getBuckets and getDroplets calls fixture.detectChanges(); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js index a44f0a130..2f9a4bd1e 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js @@ -54,7 +54,7 @@ import { } from 'services/nf-registry.auth-guard.service'; import NfRegistryImportVersionedFlow from './components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; import NfRegistryImportNewFlow from './components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; -import NfRegistryDownloadVersionedFlow from './components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; +import NfRegistryExportVersionedFlow from './components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow'; function NfRegistryModule() { } @@ -93,7 +93,7 @@ NfRegistryModule.annotations = [ NfPageNotFoundComponent, NfLoginComponent, NfUserLoginComponent, - NfRegistryDownloadVersionedFlow, + NfRegistryExportVersionedFlow, NfRegistryImportVersionedFlow, NfRegistryImportNewFlow ], @@ -106,7 +106,7 @@ NfRegistryModule.annotations = [ NfRegistryAddPolicyToBucket, NfRegistryEditBucketPolicy, NfUserLoginComponent, - NfRegistryDownloadVersionedFlow, + NfRegistryExportVersionedFlow, NfRegistryImportVersionedFlow, NfRegistryImportNewFlow ], diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 8c4eec05a..e2e4a774b 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -90,7 +90,7 @@ NfRegistryApi.prototype = { observe: 'response' }; - return this.http.get(url, options).pipe( + return self.http.get(url, options).pipe( map(function (response) { // export the VersionedFlowSnapshot by creating a hidden anchor element var stringSnapshot = encodeURIComponent(JSON.stringify(response.body, null, 2)); @@ -165,20 +165,20 @@ NfRegistryApi.prototype = { var url = '../nifi-registry-api/' + bucketUri + '/flows'; var flow = { 'name': name, 'description': description }; - return this.http.post(url, flow, headers).pipe( + // first, create Flow version 0 + return self.http.post(url, flow, headers).pipe( take(1), - // create Flow version 0 switchMap(function (response) { var flowUri = response.link.href; var importVersionUrl = '../nifi-registry-api/' + flowUri + '/versions/import'; - // import file as Flow version 1 + // then, import file as Flow version 1 return self.http.post(importVersionUrl, file, headers).pipe( map(function (snapshot) { return snapshot; }), catchError(function (error) { - // delete flow version 0 + // delete Flow version 0 var deleteUri = flowUri + '?versions=0'; self.deleteDroplet(deleteUri).subscribe(function (response) { return response; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js index bd942c2b7..8b1d3dc81 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js @@ -1400,4 +1400,166 @@ describe('NfRegistry API w/ Angular testing utils', function () { // Finally, assert that there are no outstanding requests. httpMock.verify(); })); + + it('should GET to export versioned snapshot.', inject([HttpTestingController], function (httpMock) { + var url = 'testUrl'; + var versionNumber = 1; + var reqUrl = '../nifi-registry-api/' + url + '/versions/' + versionNumber + '/export'; + + var response = '{' + + 'body: {' + + 'flowContents: {' + + 'componentType: \'PROCESS_GROUP\',' + + 'connections: [],' + + 'controllerServices: [],' + + 'funnels: [],' + + 'identifier: \'123\',' + + 'inputPorts: [],' + + 'labels: [],' + + 'name: \'Test snapshot\',' + + 'outputPorts: [],' + + 'processGroups: []' + + '},' + + 'snapshotMetadata: {' + + 'author: \'anonymous\',' + + 'bucketIdentifier: \'123\',' + + 'comments: \'Test comments\',' + + 'flowIdentifier: \'555\',' + + 'link: {' + + 'href: \'buckets/123/flows/555/versions/2\',' + + 'params: {}' + + '},' + + 'version: 2' + + '}' + + '},' + + 'headers: {' + + 'headers: [' + + '{\'filename\': [\'Test-flow-version-1\']}' + + '],' + + 'normalizedNames: {\'filename\': \'filename\'}' + + '},' + + 'ok: true,' + + 'status: 200,' + + 'statusText: \'OK\',' + + 'type: 4,' + + 'url: \'testUrl\'' + + '}'; + + var stringResponse = encodeURIComponent(response); + + var anchor = document.createElement('a'); + + anchor.href = 'data:application/json;charset=utf-8,' + stringResponse; + anchor.download = 'Test-flow-version-3.json'; + anchor.style = 'display: none;'; + + spyOn(document.body, 'appendChild'); + spyOn(document.body, 'removeChild'); + + // api call + nfRegistryApi.exportDropletVersionedSnapshot(url, versionNumber).subscribe(function (res) { + expect(res.body).toEqual(response); + expect(res.status).toEqual(200); + }); + + // the request it made + req = httpMock.expectOne(reqUrl); + expect(req.request.method).toEqual('GET'); + + // Next, fulfill the request by transmitting a response. + req.flush(response); + + // Finally, assert that there are no outstanding requests. + httpMock.verify(); + + expect(document.body.appendChild).toHaveBeenCalled(); + expect(document.body.removeChild).toHaveBeenCalled(); + })); + + it('should POST to upload versioned flow snapshot.', inject([HttpTestingController], function (httpMock) { + var url = 'testUrl'; + var reqUrl = '../nifi-registry-api/' + url + '/versions/import'; + + var response = { + flowContents: { + componentType: 'PROCESS_GROUP', + connections: [], + controllerServices: [], + funnels: [], + name: 'Test name', + identifier: '123' + }, + snapshotMetadata: { + author: 'anonymous', + comments: 'This is snapshot #5', + timestamp: 1619806926583, + version: 3 + } + }; + + var testFile = new File([], 'filename'); + + // api call + nfRegistryApi.uploadVersionedFlowSnapshot(url, testFile, '').subscribe(function (res) { + expect(res).toEqual(response); + expect(res.flowContents.name).toEqual('Test name'); + expect(res.snapshotMetadata.comments).toEqual('This is snapshot #5'); + }); + + // the request it made + req = httpMock.expectOne(reqUrl); + expect(req.request.method).toEqual('POST'); + + // Next, fulfill the request by transmitting a response. + req.flush(response); + + // Finally, assert that there are no outstanding requests. + httpMock.verify(); + })); + + it('should POST to upload new flow snapshot.', inject([HttpTestingController], function (httpMock) { + var bucketUri = 'buckets/123'; + var flowUri = 'buckets/123/flows/456'; + var createFlowReqUrl = '../nifi-registry-api/' + bucketUri + '/flows'; + var importFlowReqUrl = '../nifi-registry-api/' + flowUri + '/versions/import'; + var headers = new Headers({'Content-Type': 'application/json'}); + + var response = { + bucketIdentifier: '123', + bucketName: 'Bucket 1', + createdTimestamp: 1620168949158, + description: 'Test description', + identifier: '456', + link: { + href: 'buckets/123/flows/456', + params: {} + }, + modifiedTimestamp: 1620175586179, + name: 'Test Flow name', + permissions: {canDelete: true, canRead: true, canWrite: true}, + type: 'Flow', + versionCount: 0 + }; + + var testFile = new File([], 'filename.json'); + + // api call + nfRegistryApi.uploadFlow(bucketUri, testFile, headers).subscribe(function (res) { + expect(res).toEqual(response); + }); + + // the request it made + req = httpMock.expectOne(createFlowReqUrl); + expect(req.request.method).toEqual('POST'); + + // Next, fulfill the request by transmitting a response. + req.flush(response); + + // the inner request it made + req = httpMock.expectOne(importFlowReqUrl); + expect(req.request.method).toEqual('POST'); + + // Finally, assert that there are no outstanding requests. + httpMock.verify(); + })); }); diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js index 96e862fec..6e2c875ec 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js @@ -21,7 +21,7 @@ import { MatDialog } from '@angular/material'; import { FdsDialogService, FdsSnackBarService } from '@nifi-fds/core'; import NfRegistryApi from 'services/nf-registry.api.js'; import NfStorage from 'services/nf-storage.service.js'; -import NfRegistryDownloadVersionedFlow from '../components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; +import NfRegistryExportVersionedFlow from '../components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow'; import NfRegistryImportVersionedFlow from '../components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; import NfRegistryImportNewFlow from '../components/explorer/grid-list/dialogs/import-new-flow/nf-registry-import-new-flow'; @@ -147,11 +147,11 @@ function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, router, } }, { - name: 'Download version', + name: 'Export version', icon: 'fa fa-download', - tooltip: 'Download flow version', + tooltip: 'Export flow version', disabled: function (droplet) { - return false; + return !droplet.permissions.canRead; } }, { @@ -462,12 +462,12 @@ NfRegistryService.prototype = { }, /** - * Opens the download version dialog. + * Opens the export version dialog. * * @param droplet The droplet object. */ - openDownloadVersionedFlowDialog: function (droplet) { - this.matDialog.open(NfRegistryDownloadVersionedFlow, { + openExportVersionedFlowDialog: function (droplet) { + this.matDialog.open(NfRegistryExportVersionedFlow, { disableClose: true, width: '400px', data: { @@ -529,9 +529,9 @@ NfRegistryService.prototype = { // Opens the import versioned flow dialog this.openImportVersionedFlowDialog(droplet); break; - case 'download version': - // Opens the download flow version dialog - this.openDownloadVersionedFlowDialog(droplet); + case 'export version': + // Opens the export flow version dialog + this.openExportVersionedFlowDialog(droplet); break; case 'delete flow': // Deletes the entire data flow diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js index 3af13220a..a21007da3 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js @@ -22,8 +22,8 @@ import NfRegistryApi from 'services/nf-registry.api'; import NfRegistryService from 'services/nf-registry.service'; import { Router } from '@angular/router'; import { FdsDialogService } from '@nifi-fds/core'; -import NfRegistryDownloadVersionedFlow - from '../components/explorer/grid-list/dialogs/download-versioned-flow/nf-registry-download-versioned-flow'; +import NfRegistryExportVersionedFlow + from '../components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow'; import NfRegistryImportVersionedFlow from '../components/explorer/grid-list/dialogs/import-versioned-flow/nf-registry-import-versioned-flow'; import NfRegistryImportNewFlow @@ -708,7 +708,12 @@ describe('NfRegistry Service w/ Angular testing utils', function () { it('should execute the `delete` droplet action.', function () { //Setup the nfRegistryService state for this test - nfRegistryService.droplets = [{identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc'}]; + nfRegistryService.droplets = [ + { + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + permissions: {canDelete: true, canRead: true, canWrite: true} + } + ]; //Spy spyOn(nfRegistryService.dialogService, 'openConfirm').and.returnValue({ @@ -725,7 +730,8 @@ describe('NfRegistry Service w/ Angular testing utils', function () { nfRegistryService.executeDropletAction({name: 'delete flow'}, { identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', type: 'testTYPE', - link: {href: 'testhref'} + link: {href: 'testhref'}, + permissions: {canDelete: true, canRead: true, canWrite: true} }); //assertions @@ -1098,19 +1104,20 @@ describe('NfRegistry Service w/ Angular testing utils', function () { expect(nfRegistryService.users[0].identifier).toBe(12); }); - it('should open the Download Version dialog.', function () { + it('should open the Export Version dialog.', function () { //Setup the nfRegistryService state for this test var droplet = { identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', type: 'testTYPE', - link: {href: 'testhref'} + link: {href: 'testhref'}, + permissions: {canDelete: true, canRead: true, canWrite: true} }; //Spy spyOn(nfRegistryService.matDialog, 'open'); - nfRegistryService.executeDropletAction({name: 'download version'}, droplet); - expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryDownloadVersionedFlow, { + nfRegistryService.executeDropletAction({name: 'export version'}, droplet); + expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryExportVersionedFlow, { disableClose: true, width: '400px', data: { @@ -1124,7 +1131,8 @@ describe('NfRegistry Service w/ Angular testing utils', function () { var droplet = { identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', type: 'testTYPE', - link: {href: 'testhref'} + link: {href: 'testhref'}, + permissions: {canDelete: true, canRead: true, canWrite: true} }; //Spy diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss index 4a4b5b1e9..6535f6ccf 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -140,7 +140,7 @@ input#upload-versioned-flow-file-field { } } -#nifi-registry-download-versioned-flow-dialog .bucket-dropdown-field .mat-select-value { +#nifi-registry-export-versioned-flow-dialog .bucket-dropdown-field .mat-select-value { color: $gray; } @@ -208,3 +208,13 @@ input[type=text]#new-flow-definition-input { text-overflow: ellipsis; cursor: pointer; } + +body[fds] .fds-primary-dropdown-button-menu.fds-primary-dropdown-button-menu .mat-menu-item:focus:not([disabled]) { + color: rgba(0, 0, 0, 0.87); + background-color: rgba(0, 0, 0, 0.04); +} + +body[fds] .fds-primary-dropdown-button-menu.fds-primary-dropdown-button-menu .mat-menu-item:hover:not([disabled]) { + color: #fff; + background-color: #915d69; +} From 62a69b88d3d8eab8cfb852bcc98d2ccacb272574 Mon Sep 17 00:00:00 2001 From: mtien Date: Wed, 12 May 2021 14:27:32 -0700 Subject: [PATCH 08/10] NIFIREG-395 - Removed JSON.stringify and return a responseType of text. --- .../src/main/webapp/services/nf-registry.api.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index e2e4a774b..9bd6a04a2 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -87,13 +87,14 @@ NfRegistryApi.prototype = { var url = '../nifi-registry-api/' + dropletUri + '/versions/' + versionNumber + '/export'; var options = { headers: headers, - observe: 'response' + observe: 'response', + responseType: 'text' }; return self.http.get(url, options).pipe( map(function (response) { // export the VersionedFlowSnapshot by creating a hidden anchor element - var stringSnapshot = encodeURIComponent(JSON.stringify(response.body, null, 2)); + var stringSnapshot = encodeURIComponent(response.body); var filename = response.headers.get('Filename'); var anchorElement = document.createElement('a'); From 24f4d05a5e18c0c2376d3c78f6de8eccc6f19d27 Mon Sep 17 00:00:00 2001 From: mtien Date: Wed, 12 May 2021 17:06:15 -0700 Subject: [PATCH 09/10] NIFIREG-395 - Updated the 'Export Version' latest menu item name --- .../export-versioned-flow/nf-registry-export-version.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html index 4ac01e9e4..2c0680a75 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html @@ -32,7 +32,7 @@ - Version {{snapshotMeta.version}} (Latest) + Latest (Version {{snapshotMeta.version}}) Version {{snapshotMeta.version}} From 6d9901afe73032fadc2f30400b47a9212db61930 Mon Sep 17 00:00:00 2001 From: mtien Date: Fri, 14 May 2021 12:50:43 -0700 Subject: [PATCH 10/10] NIFIREG-395 - Added a Location header for a created Response - Updated comments header --- .../org/apache/nifi/registry/web/api/BucketFlowResource.java | 5 ++++- .../org/apache/nifi/registry/web/api/HttpStatusMessages.java | 3 +++ .../src/main/webapp/services/nf-registry.api.js | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 067b3078c..fd0d9cb76 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -24,6 +24,7 @@ import io.swagger.annotations.Authorization; import io.swagger.annotations.Extension; import io.swagger.annotations.ExtensionProperty; +import java.net.URI; import javax.ws.rs.HeaderParam; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; @@ -310,6 +311,7 @@ public Response createFlowVersion( } ) @ApiResponses({ + @ApiResponse(code = 201, message = HttpStatusMessages.MESSAGE_201), @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), @@ -327,7 +329,8 @@ public Response importVersionedFlow( final VersionedFlowSnapshot createdSnapshot = serviceFacade.importVersionedFlowSnapshot(versionedFlowSnapshot, bucketId, flowId, comments); publish(EventFactory.flowVersionCreated(createdSnapshot)); - return Response.status(Response.Status.CREATED).entity(createdSnapshot).build(); + String locationUri = createdSnapshot.getSnapshotMetadata().getLink().getUri().getPath(); + return generateCreatedResponse(URI.create(locationUri), createdSnapshot).build(); } @GET diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java index a3ba939d2..3ad422b62 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java @@ -18,6 +18,9 @@ class HttpStatusMessages { + /* 2xx messages */ + static final String MESSAGE_201 = "The resource has been successfully created."; + /* 4xx messages */ static final String MESSAGE_400 = "NiFi Registry was unable to complete the request because it was invalid. The request should not be retried without modification."; static final String MESSAGE_401 = "Client could not be authenticated."; diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 9bd6a04a2..6c48004d4 100644 --- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js @@ -133,7 +133,7 @@ NfRegistryApi.prototype = { var url = '../nifi-registry-api/' + dropletUri + '/versions/import'; var versionHeaders = new HttpHeaders() .set('Content-Type', 'application/json') - .set('comments', comments); + .set('Comments', comments); return self.http.post(url, file, { 'headers': versionHeaders }).pipe( map(function (response) {