From 69c10f5a69811d21de5bfff634ab85202e5738ae Mon Sep 17 00:00:00 2001 From: M Tien <56892372+mtien-apache@users.noreply.github.com> Date: Wed, 9 Jun 2021 08:26:04 -0700 Subject: [PATCH] NIFIREG-395 - Implemented the ability to import and export versioned flows through the UI. (#5107) - Added REST endpoints in BucketFlowResource for importVersionedFlow and exportVersionedFlow. - Added import and export dialogs. - Set the initial value for the Bucket dropdown menu in the Import dialogs. - 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. - Updated Flow Actions menu hover and focus styles. --- .../nifi-registry-web-api/pom.xml | 9 + .../registry/web/api/BucketFlowResource.java | 83 +++++++ .../registry/web/api/HttpStatusMessages.java | 3 + .../ExportedVersionedFlowSnapshot.java | 65 ++++++ .../registry/web/service/ServiceFacade.java | 4 + .../web/service/StandardServiceFacade.java | 44 ++++ .../apache/nifi/registry/web/api/FlowsIT.java | 172 ++++++++++++++ .../test-versioned-flow-snapshot.json | 23 ++ .../nf-registry-export-version.html | 53 +++++ .../nf-registry-export-versioned-flow.js | 96 ++++++++ .../nf-registry-export-versioned-flow.spec.js | 141 +++++++++++ .../nf-registry-import-new-flow.html | 135 +++++++++++ .../nf-registry-import-new-flow.js | 191 +++++++++++++++ .../nf-registry-import-new-flow.spec.js | 96 ++++++++ .../nf-registry-import-versioned-flow.html | 121 ++++++++++ .../nf-registry-import-versioned-flow.js | 153 ++++++++++++ .../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.html | 30 ++- .../nf-registry-grid-list-viewer.spec.js | 9 +- .../src/main/webapp/nf-registry.module.js | 13 +- .../main/webapp/services/nf-registry.api.js | 134 ++++++++++- .../webapp/services/nf-registry.api.spec.js | 162 +++++++++++++ .../webapp/services/nf-registry.service.js | 199 ++++++++++++---- .../services/nf-registry.service.spec.js | 146 ++++++++++-- .../explorer/dialogs/_structureElements.scss | 220 ++++++++++++++++++ .../grid-list/_structureElements.scss | 5 + .../src/main/webapp/theming/nf-registry.scss | 1 + 29 files changed, 2356 insertions(+), 81 deletions(-) create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js create mode 100644 nifi-registry/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/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/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/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/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/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/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 create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml index 132ad93323..fdfed30b24 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml +++ b/nifi-registry/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 + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 8dd7290432..fd0d9cb76a 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -24,11 +24,15 @@ import io.swagger.annotations.ApiResponses; 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; import org.apache.nifi.registry.bucket.BucketItem; 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; @@ -291,6 +295,44 @@ public class BucketFlowResource extends ApplicationResource { return Response.status(Response.Status.OK).entity(createdSnapshot).build(); } + @POST + @Path("{flowId}/versions/import") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + 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 = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @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), + @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, + @ApiParam("file") final VersionedFlowSnapshot versionedFlowSnapshot, + @HeaderParam("Comments") final String comments) { + + final VersionedFlowSnapshot createdSnapshot = serviceFacade.importVersionedFlowSnapshot(versionedFlowSnapshot, bucketId, flowId, comments); + publish(EventFactory.flowVersionCreated(createdSnapshot)); + String locationUri = createdSnapshot.getSnapshotMetadata().getLink().getUri().getPath(); + return generateCreatedResponse(URI.create(locationUri), createdSnapshot).build(); + } + @GET @Path("{flowId}/versions") @Consumes(MediaType.WILDCARD) @@ -385,6 +427,47 @@ public class BucketFlowResource extends ApplicationResource { 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) { + + final ExportedVersionedFlowSnapshot exportedVersionedFlowSnapshot = serviceFacade.exportFlowSnapshot(bucketId, flowId, versionNumber); + + final VersionedFlowSnapshot versionedFlowSnapshot = exportedVersionedFlowSnapshot.getVersionedFlowSnapshot(); + + final String contentDisposition = String.format( + "attachment; filename=\"%s\"", + exportedVersionedFlowSnapshot.getFilename()); + + return generateOkResponse(versionedFlowSnapshot) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) + .header("Filename", exportedVersionedFlowSnapshot.getFilename()) + .build(); + } + @GET @Path("{flowId}/versions/{versionNumber: \\d+}") @Consumes(MediaType.WILDCARD) diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java index a3ba939d29..3ad422b623 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java @@ -18,6 +18,9 @@ package org.apache.nifi.registry.web.api; 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/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ExportedVersionedFlowSnapshot.java new file mode 100644 index 0000000000..b81a65bae5 --- /dev/null +++ b/nifi-registry/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/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java index b1faac46c0..85ac6e7472 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java @@ -102,6 +102,10 @@ public interface ServiceFacade { VersionedFlowSnapshot getLatestFlowSnapshot(String flowIdentifier); + VersionedFlowSnapshot importVersionedFlowSnapshot(VersionedFlowSnapshot versionedFlowSnapshot, String bucketIdentifier, String flowIdentifier, String comments); + + ExportedVersionedFlowSnapshot exportFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer versionNumber); + SortedSet getFlowSnapshots(String bucketIdentifier, String flowIdentifier); SortedSet getFlowSnapshots(String flowIdentifier); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java index 85be421f1f..dec1a87f7e 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -109,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, @@ -360,6 +362,48 @@ public class StandardServiceFacade implements ServiceFacade { 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(LATEST_VERSION); + + // if there are new comments, then set it + // otherwise, keep the original comments + if (StringUtils.isNotBlank(comments)) { + metadata.setComments(comments); + } else if (versionedFlowSnapshot.getSnapshotMetadata() != null && StringUtils.isNotBlank(versionedFlowSnapshot.getSnapshotMetadata().getComments())) { + metadata.setComments(versionedFlowSnapshot.getSnapshotMetadata().getComments()); + } + + versionedFlowSnapshot.setSnapshotMetadata(metadata); + + final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + versionedFlowSnapshot.getSnapshotMetadata().setAuthor(userIdentity); + + return createFlowSnapshot(versionedFlowSnapshot); + } + + @Override + 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 new ExportedVersionedFlowSnapshot(versionedFlowSnapshot, filename); + } + @Override public SortedSet getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) { authorizeBucketAccess(RequestAction.READ, bucketIdentifier); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java index 4471494b9e..b04f872165 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java +++ b/nifi-registry/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,16 +35,20 @@ import javax.ws.rs.client.WebTarget; 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"}) public class FlowsIT extends UnsecuredITBase { + private static final int LATEST_VERSION = -1; + @Test public void testGetFlowsEmpty() throws Exception { @@ -541,4 +546,171 @@ public class FlowsIT extends UnsecuredITBase { } + @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(LATEST_VERSION); + + 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(LATEST_VERSION); + + 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/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/test-versioned-flow-snapshot.json new file mode 100644 index 0000000000..647ca5d5bd --- /dev/null +++ b/nifi-registry/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/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html new file mode 100644 index 0000000000..2c0680a756 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-version.html @@ -0,0 +1,53 @@ + + +
+
+ + Export Version + + +
+
+
+ +
+
+ + + + Latest (Version {{snapshotMeta.version}}) + Version {{snapshotMeta.version}} + + + +
+
+
+ + + +
+
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow.js new file mode 100644 index 0000000000..beb40c5a5d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-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'; + +/** + * NfRegistryExportVersionedFlow 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 NfRegistryExportVersionedFlow(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; +} + +NfRegistryExportVersionedFlow.prototype = { + constructor: NfRegistryExportVersionedFlow, + + /** + * Export specified versioned flow snapshot. + */ + exportVersion: 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 an export of a version and close dialog. + */ + cancel: function () { + this.dialogRef.close(); + } +}; + +NfRegistryExportVersionedFlow.annotations = [ + new Component({ + templateUrl: './nf-registry-export-version.html' + }) +]; + +NfRegistryExportVersionedFlow.parameters = [ + NfRegistryApi, + FdsSnackBarService, + MatDialogRef, + MAT_DIALOG_DATA +]; + +export default NfRegistryExportVersionedFlow; diff --git a/nifi-registry/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/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 0000000000..6d49ec7432 --- /dev/null +++ b/nifi-registry/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/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/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 0000000000..9bc0b516e0 --- /dev/null +++ b/nifi-registry/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/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/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 0000000000..978a79667c --- /dev/null +++ b/nifi-registry/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,191 @@ +/* + * 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.buckets = data.buckets; + this.activeBucket = data.activeBucket.identifier; + 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 () { + 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 (this.activeBucket === undefined) { + if (this.writableBuckets.length === 1) { + // set the active bucket + this.activeBucket = this.writableBuckets[0].identifier; + } + } + }, + + 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.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 === 201) { + 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/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/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 0000000000..b315a28dd5 --- /dev/null +++ b/nifi-registry/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/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/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 0000000000..4d7201b83c --- /dev/null +++ b/nifi-registry/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,121 @@ + + +
+
+ + Import New Version + + +
+
+
+
+ +
+
+
+ +
+
+ v.{{droplet.snapshotMetadata.length + 1}} +
+
+
+
+
+ +
+ + + Looks good! + + + + + + File format is not valid + + +
+
+ + +
+ + Select file +
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + + +
diff --git a/nifi-registry/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/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 0000000000..a41c89c92e --- /dev/null +++ b/nifi-registry/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,153 @@ +/* + * 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.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 === 201) { + 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/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/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 0000000000..ae3b50457a --- /dev/null +++ b/nifi-registry/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/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.spec.js index 21b5553de6..0f47ac166c 100644 --- a/nifi-registry/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/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/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.spec.js index 40d9293226..6965762f51 100644 --- a/nifi-registry/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/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html index e0faa83c45..30eb3b20dc 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html +++ b/nifi-registry/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 @@ limitations under the License.
-
- +
Sort by:
{{nfRegistryService.getSortByLabel()}}
+
+ +
+
+
+ You are not authorized to import flows. +
@@ -64,9 +75,9 @@ limitations under the License.
@@ -118,7 +129,10 @@ limitations under the License.
-
+

No results match this query.

+
+

There are no buckets to display.

+
diff --git a/nifi-registry/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.spec.js index 117f635ae9..381327ad3c 100644 --- a/nifi-registry/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/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js index 59c7262f36..2f9a4bd1e4 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.module.js +++ b/nifi-registry/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 NfRegistryExportVersionedFlow from './components/explorer/grid-list/dialogs/export-versioned-flow/nf-registry-export-versioned-flow'; function NfRegistryModule() { } @@ -89,7 +92,10 @@ NfRegistryModule.annotations = [ NfRegistryDropletGridListViewer, NfPageNotFoundComponent, NfLoginComponent, - NfUserLoginComponent + NfUserLoginComponent, + NfRegistryExportVersionedFlow, + NfRegistryImportVersionedFlow, + NfRegistryImportNewFlow ], entryComponents: [ NfRegistryAddUser, @@ -99,7 +105,10 @@ NfRegistryModule.annotations = [ NfRegistryAddUsersToGroup, NfRegistryAddPolicyToBucket, NfRegistryEditBucketPolicy, - NfUserLoginComponent + NfUserLoginComponent, + NfRegistryExportVersionedFlow, + NfRegistryImportVersionedFlow, + NfRegistryImportNewFlow ], providers: [ NfRegistryService, diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js index 99cacf8e38..6c48004d44 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js +++ b/nifi-registry/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, take, switchMap } from 'rxjs/operators'; var MILLIS_PER_SECOND = 1000; var headers = new Headers({'Content-Type': 'application/json'}); @@ -75,6 +75,138 @@ 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 + '/versions/' + versionNumber + '/export'; + var options = { + headers: headers, + 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(response.body); + var filename = response.headers.get('Filename'); + + var anchorElement = document.createElement('a'); + anchorElement.href = 'data:application/json;charset=utf-8,' + stringSnapshot; + anchorElement.download = filename; + anchorElement.style = 'display: none;'; + + document.body.appendChild(anchorElement); + anchorElement.click(); + document.body.removeChild(anchorElement); + + 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 + '/versions/import'; + var versionHeaders = new HttpHeaders() + .set('Content-Type', 'application/json') + .set('Comments', comments); + + return self.http.post(url, file, { 'headers': versionHeaders }).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 + '/flows'; + var flow = { 'name': name, 'description': description }; + + // first, create Flow version 0 + return self.http.post(url, flow, headers).pipe( + take(1), + switchMap(function (response) { + var flowUri = response.link.href; + var importVersionUrl = '../nifi-registry-api/' + flowUri + '/versions/import'; + + // 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 + 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({ + 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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js index bd942c2b74..8b1d3dc818 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.spec.js +++ b/nifi-registry/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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js index 8e1241b795..6e2c875ec6 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js +++ b/nifi-registry/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 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'; /** * 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: 'Export version', + icon: 'fa fa-download', + tooltip: 'Export flow version', + disabled: function (droplet) { + return !droplet.permissions.canRead; + } + }, + { + name: 'Delete flow', icon: 'fa fa-trash', - tooltip: 'Delete' + tooltip: 'Delete', + disabled: function (droplet) { + return !droplet.permissions.canDelete; + } } ]; this.disableMultiDeleteAction = false; @@ -393,6 +418,105 @@ 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 Flow', + 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 export version dialog. + * + * @param droplet The droplet object. + */ + openExportVersionedFlowDialog: function (droplet) { + this.matDialog.open(NfRegistryExportVersionedFlow, { + disableClose: true, + width: '400px', + data: { + droplet: droplet + } + }); + }, + + /** + * Opens the import new flow dialog. + * + * @param buckets The buckets object. + * @param activeBucket The active bucket object. + */ + openImportNewFlowDialog: function (buckets, activeBucket) { + var self = this; + this.matDialog.open(NfRegistryImportNewFlow, { + disableClose: true, + width: '550px', + data: { + buckets: buckets, + activeBucket: activeBucket + } + }).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 +524,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 'export version': + // Opens the export flow version dialog + this.openExportVersionedFlowDialog(droplet); + break; + case 'delete flow': + // Deletes the entire data flow + this.deleteDroplet(droplet); + break; + default: // do nothing } }, @@ -633,6 +735,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 +1331,8 @@ NfRegistryService.parameters = [ TdDataTableService, Router, FdsDialogService, - FdsSnackBarService + FdsSnackBarService, + MatDialog ]; export default NfRegistryService; diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js index 455963d3c0..a21007da33 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js +++ b/nifi-registry/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 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'; describe('NfRegistry Service isolated unit tests', function () { @@ -702,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({ @@ -716,17 +727,18 @@ describe('NfRegistry Service w/ Angular testing utils', function () { }); // The function to test - nfRegistryService.executeDropletAction({name: 'delete'}, { + 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 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 +807,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 +862,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 +1035,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 +1077,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 +1103,112 @@ 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 Export Version dialog.', function () { + //Setup the nfRegistryService state for this test + var droplet = { + identifier: '2e04b4fb-9513-47bb-aa74-1ae34616bfdc', + type: 'testTYPE', + link: {href: 'testhref'}, + permissions: {canDelete: true, canRead: true, canWrite: true} + }; + + //Spy + spyOn(nfRegistryService.matDialog, 'open'); + + nfRegistryService.executeDropletAction({name: 'export version'}, droplet); + expect(nfRegistryService.matDialog.open).toHaveBeenCalledWith(NfRegistryExportVersionedFlow, { + 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'}, + permissions: {canDelete: true, canRead: true, canWrite: true} + }; + + //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/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss new file mode 100644 index 0000000000..6535f6ccf4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/dialogs/_structureElements.scss @@ -0,0 +1,220 @@ +/* + * 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; +$dark-gray: #ced3d7; +$teal-gray: #6b8791; + +.label-name { + font-weight: 500; + color: $gray; +} + +.version-count { + font-weight: 500; + padding: 9px 18px; + margin-left: 5px; + border-radius: 17px; + background-color: #eee; + color: $gray; +} + +#flow-version-name { + border: none; + height: 34px; + outline: 0; + font-family: Roboto, sans-serif; + font-size: 14px; + color: $gray; + + span { + overflow: hidden; + } +} + +#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: $teal-gray; + padding: 9px 5px; + outline: none; + } + + span { + text-transform: uppercase; + color: $teal-gray; + } +} + +input#upload-flow-file-field, +input#upload-versioned-flow-file-field { + display: none; +} + +#flow-version-comments { + padding-bottom: 26px; + + textarea { + width: 515px; + height: 48px; + border: solid 1px #cfd3d7; + border-radius: 2px; + padding: 9px 16px 9px 12px; + resize: none; + color: $gray; + } +} + +#new-flow-description { + textarea { + width: 515px; + height: 48px; + border: solid 1px #cfd3d7; + border-radius: 2px; + padding: 9px 16px 9px 12px; + color: $gray; + } +} + +#flow-version-comments, +#new-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 { + font-size: 12px; + color: $light-gray; + } + + i { + font-size: 15px; + } +} + +#nifi-registry-export-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-value-text { + color: $gray; + } + + .mat-select-placeholder { + font-weight: 300; + } + } + + .mat-select-arrow-wrapper { + transform: none; + } + + .mat-form-field-flex { + border: 1px solid $dark-gray; + background-color: transparent; + border-radius: 2px; + padding-top: 0; + } +} + +.bucket-dropdown-select { + .mat-select-panel { + color: $gray; + } +} + +#new-flow-definition, +#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-flow-container .mat-form-field-appearance-fill .mat-form-field-infix { + 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; +} + +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; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss index 68e26b592e..9b4b7ac8be 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/components/explorer/grid-list/_structureElements.scss +++ b/nifi-registry/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; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss b/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss index 37d9ea4977..740aa7d223 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/nf-registry.scss +++ b/nifi-registry/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