mirror of https://github.com/apache/nifi.git
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.
This commit is contained in:
parent
8641b54a49
commit
69c10f5a69
|
@ -247,6 +247,15 @@
|
|||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.rat</groupId>
|
||||
<artifactId>apache-rat-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes combine.children="append">
|
||||
<exclude>src/test/resources/test-versioned-flow-snapshot.json</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
|
@ -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<VersionedFlowSnapshotMetadata> getFlowSnapshots(String bucketIdentifier, String flowIdentifier);
|
||||
|
||||
SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(String flowIdentifier);
|
||||
|
|
|
@ -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<VersionedFlowSnapshotMetadata> getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) {
|
||||
authorizeBucketAccess(RequestAction.READ, bucketIdentifier);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<div id="nifi-registry-export-versioned-flow-dialog">
|
||||
<div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<mat-card-title>
|
||||
Export Version
|
||||
</mat-card-title>
|
||||
<button mat-icon-button (click)="cancel()">
|
||||
<mat-icon color="primary">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayoutAlign="space-between start" class="pad-bottom-sm">
|
||||
<div class="pad-bottom-sm label-name">
|
||||
<label>Choose Version</label>
|
||||
</div>
|
||||
<div class="fill-available-width bucket-dropdown-field">
|
||||
<mat-form-field appearance="fill" fxFlex>
|
||||
<mat-select panelClass="bucket-dropdown-select" [(value)]="selectedVersion">
|
||||
<mat-option *ngFor="let snapshotMeta of droplet.snapshotMetadata" [value]="snapshotMeta.version">
|
||||
<span *ngIf="snapshotMeta === droplet.snapshotMetadata[0]">Latest (Version {{snapshotMeta.version}})</span>
|
||||
<span *ngIf="snapshotMeta != droplet.snapshotMetadata[0]">Version {{snapshotMeta.version}}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div fxLayout="row">
|
||||
<span fxFlex></span>
|
||||
<button (click)="cancel()" color="fds-regular" mat-raised-button
|
||||
i18n="Cancel export of versioned flow selection|A button for cancelling the versioned flow selection to export in the registry.">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="push-left-sm" data-automation-id="export-versioned-flow-button" (click)="exportVersion()"
|
||||
color="fds-primary" mat-raised-button i18n="Cancel export of versioned flow selection|A button for cancelling the versioned flow selection to export in the registry.">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,135 @@
|
|||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<div id="nifi-registry-import-new-flow-dialog"
|
||||
xmlns:width="http://www.w3.org/1999/xhtml">
|
||||
<div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<mat-card-title class="ellipsis">
|
||||
Import New Flow
|
||||
</mat-card-title>
|
||||
<button mat-icon-button (click)="cancel()">
|
||||
<mat-icon color="primary">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div id="new-flow-container">
|
||||
<div class="fill-available-width">
|
||||
<div class="pad-bottom-sm label-name">
|
||||
<label>Flow Name</label>
|
||||
</div>
|
||||
<div id="flow-name" fxLayout="row" fxLayoutAlign="space-between center" class="push-bottom-md">
|
||||
<div id="new-flow-name-input" class="fill-available-width">
|
||||
<input
|
||||
#flowName
|
||||
id="new-flow-name-input-field"
|
||||
class="pad-bottom-md fill-available-width"
|
||||
matInput
|
||||
type="text"
|
||||
placeholder="Create a unique name"
|
||||
[(ngModel)]="name"/>
|
||||
</div>
|
||||
<div class="version-count">
|
||||
<span>v.1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pad-bottom-sm label-name">
|
||||
<label class="pad-bottom-sm">Flow Description</label>
|
||||
</div>
|
||||
<div id="new-flow-description" fxLayout="row" class="pad-bottom-xs fill-available-width">
|
||||
<textarea class="push-bottom-md" [(ngModel)]="description"></textarea>
|
||||
</div>
|
||||
<div class="pad-bottom-sm label-name">
|
||||
<label>Bucket</label>
|
||||
</div>
|
||||
<div class="fill-available-width bucket-dropdown-field pad-bottom-xs">
|
||||
<mat-form-field appearance="fill" floatLabel="always" fxFlex>
|
||||
<mat-select #bucketSelect panelClass="bucket-dropdown-select" placeholder="Choose a location" [(value)]="activeBucket">
|
||||
<mat-option *ngFor="let bucket of writableBuckets" [value]="bucket.identifier">
|
||||
{{bucket.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div id="new-flow-file-upload-message-container" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div class="label-name">
|
||||
<label>Flow Definition</label>
|
||||
</div>
|
||||
<ng-container *ngIf="fileToUpload != null && !hoverValidity || hoverValidity === 'valid'">
|
||||
<span class="file-upload-message">
|
||||
Looks good!
|
||||
<i class="fa fa-check-circle-o" aria-hidden="true"
|
||||
style="color: #1eb475;"></i>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hoverValidity === 'invalid'">
|
||||
<span class="file-upload-message">
|
||||
File format is not valid
|
||||
<i class="fa fa-times-circle" aria-hidden="true"
|
||||
style="color: #ef6162;"></i></span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div id="new-flow-definition"
|
||||
class="fill-available-width pad-bottom-sm"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
(click)="selectFile()"
|
||||
(dragenter)="fileDragHandler($event, extensions)"
|
||||
(dragover)="fileDragHandler($event, extensions)"
|
||||
(dragend)="fileDragEndHandler()"
|
||||
(dragleave)="fileDragEndHandler()"
|
||||
(drop)="fileDropHandler($event)">
|
||||
<mat-form-field floatLabel="never" flex>
|
||||
<input matInput
|
||||
id="new-flow-definition-input"
|
||||
class="ellipsis"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Drop file or select..."
|
||||
[value]="fileName"
|
||||
[ngClass]="{'file-hover-valid': (hoverValidity === 'valid'),
|
||||
'file-hover-error': (hoverValidity === 'invalid'),
|
||||
'file-selected': (fileToUpload != null), 'multiple': multiple}"/>
|
||||
<div class="icon" id="select-flow-file-button">
|
||||
<i class="fa fa-upload" aria-hidden="true"></i>
|
||||
<span>Select file</span>
|
||||
</div>
|
||||
<div id="new-flow-file-upload-form-container">
|
||||
<form id="new-flow-file-upload-form" enctype="multipart/form-data" method="post">
|
||||
<input id="upload-flow-file-field"
|
||||
type="file"
|
||||
name="file"
|
||||
[accept]="extensions"
|
||||
(change)="handleFileInput($event.target.files)"/>
|
||||
</form>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div fxLayout="row">
|
||||
<span fxFlex></span>
|
||||
<button (click)="cancel()" color="fds-regular" mat-raised-button
|
||||
i18n="Cancel new flow definition import|A button for cancelling the new flow definition to import in the registry.">
|
||||
Cancel
|
||||
</button>
|
||||
<button [disabled]="!fileToUpload || !flowName.value || !bucketSelect.value" class="push-left-sm" data-automation-id="import-new-flow-button"
|
||||
(click)="importNewFlow()"
|
||||
color="fds-primary" mat-raised-button
|
||||
i18n="Cancel new flow definition import|A button for cancelling the new flow definition to import in the registry.">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<div id="nifi-registry-import-versioned-flow-dialog"
|
||||
xmlns:width="http://www.w3.org/1999/xhtml">
|
||||
<div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<mat-card-title>
|
||||
Import New Version
|
||||
</mat-card-title>
|
||||
<button mat-icon-button (click)="cancel()">
|
||||
<mat-icon color="primary">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div id="flow-version-container">
|
||||
<div class="fill-available-width">
|
||||
<div class="pad-bottom-sm label-name">
|
||||
<label>Flow Name</label>
|
||||
</div>
|
||||
<div id="flow-version-name" fxLayout="row" fxLayoutAlign="space-between center" class="push-bottom-md">
|
||||
<div id="version-flow-name-input" class="fill-available-width">
|
||||
<input
|
||||
id="version-flow-name-input-field"
|
||||
class="pad-bottom-md fill-available-width"
|
||||
matInput
|
||||
[disabled]="true"
|
||||
type="text"
|
||||
value="{{droplet.name}}"/>
|
||||
</div>
|
||||
<div class="version-count">
|
||||
<span>v.{{droplet.snapshotMetadata.length + 1}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="versioned-flow-file-upload-message-container" fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<div class="pad-bottom-xs label-name">
|
||||
<label>Flow Definition</label>
|
||||
</div>
|
||||
<ng-container *ngIf="fileToUpload != null && !hoverValidity || hoverValidity === 'valid'">
|
||||
<span class="file-upload-message">
|
||||
Looks good!
|
||||
<i class="fa fa-check-circle-o" aria-hidden="true"
|
||||
style="color: #1eb475;"></i>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hoverValidity === 'invalid'">
|
||||
<span class="file-upload-message">
|
||||
File format is not valid
|
||||
<i class="fa fa-times-circle" aria-hidden="true"
|
||||
style="color: #ef6162;"></i></span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div id="flow-version-definition"
|
||||
class="fill-available-width"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
(click)="selectFile()"
|
||||
(dragenter)="fileDragHandler($event, extensions)"
|
||||
(dragover)="fileDragHandler($event, extensions)"
|
||||
(dragend)="fileDragEndHandler()"
|
||||
(dragleave)="fileDragEndHandler()"
|
||||
(drop)="fileDropHandler($event)">
|
||||
<mat-form-field floatLabel="never" flex>
|
||||
<input matInput
|
||||
id="flow-version-definition-input"
|
||||
type="text"
|
||||
[value]="fileName"
|
||||
placeholder="Drop file or select..."
|
||||
autocomplete="off"
|
||||
[ngClass]="{'file-hover-valid': (hoverValidity === 'valid'),
|
||||
'file-hover-error': (hoverValidity === 'invalid'),
|
||||
'file-selected': (fileToUpload != null), 'multiple': multiple}"/>
|
||||
<div id="select-flow-version-file-button" title="Browse">
|
||||
<i class="fa fa-upload" aria-hidden="true"></i>
|
||||
<span>Select file</span>
|
||||
</div>
|
||||
<div id="versioned-flow-file-upload-form-container">
|
||||
<form id="versioned-flow-file-upload-form" enctype="multipart/form-data" method="post">
|
||||
<input id="upload-versioned-flow-file-field"
|
||||
type="file"
|
||||
name="file"
|
||||
[accept]="extensions"
|
||||
(change)="handleFileInput($event.target.files)"/>
|
||||
</form>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="pad-bottom-sm">
|
||||
<label class="pad-bottom-sm label-name">Version Comments</label>
|
||||
</div>
|
||||
<div id="flow-version-comments" fxLayout="row" class="fill-available-width">
|
||||
<textarea [(ngModel)]="comments"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div fxLayout="row">
|
||||
<span fxFlex></span>
|
||||
<button (click)="cancel()" color="fds-regular" mat-raised-button
|
||||
i18n="Cancel new flow version import|A button for cancelling the new flow version to import in the registry.">
|
||||
Cancel
|
||||
</button>
|
||||
<button [disabled]="!fileToUpload" class="push-left-sm" data-automation-id="import-new-versioned-flow-button"
|
||||
(click)="importNewVersion()"
|
||||
color="fds-primary" mat-raised-button
|
||||
i18n="Cancel new flow version import|A button for cancelling the new flow version to import in the registry.">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -17,17 +17,19 @@ limitations under the License.
|
|||
|
||||
<div class="pad-top-sm pad-bottom-sm pad-right-xxl pad-left-xxl">
|
||||
<div layout="row" layout-align="space-between center">
|
||||
<div flex fxLayout="row" fxLayoutAlign="end center">
|
||||
<td-chips [(ngModel)]="nfRegistryService.dropletsSearchTerms"
|
||||
[items]="nfRegistryService.autoCompleteDroplets"
|
||||
(add)="nfRegistryService.filterDroplets(nfRegistryService.activeDropletColumn.name, nfRegistryService.activeDropletColumn.sortOrder);"
|
||||
(remove)="nfRegistryService.filterDroplets(nfRegistryService.activeDropletColumn.name, nfRegistryService.activeDropletColumn.sortOrder);"class="push-right-sm"></td-chips>
|
||||
<div flex fxLayout="row" fxLayoutAlign="start center">
|
||||
<span class="push-top-sm pad-right-sm">Sort by:</span>
|
||||
<div fxLayout="row" fxLayoutAlign="end center" [matMenuTriggerFor]="dropletGridSortMenu">
|
||||
<div class="push-top-sm" id="droplet-sort-by-field">{{nfRegistryService.getSortByLabel()}}</div>
|
||||
<i class="push-top-sm fa fa-caret-down pad-left-sm" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div flex fxLayout="row" fxLayoutAlign="end center">
|
||||
<td-chips [(ngModel)]="nfRegistryService.dropletsSearchTerms"
|
||||
[items]="nfRegistryService.autoCompleteDroplets"
|
||||
(add)="nfRegistryService.filterDroplets(nfRegistryService.activeDropletColumn.name, nfRegistryService.activeDropletColumn.sortOrder);"
|
||||
(remove)="nfRegistryService.filterDroplets(nfRegistryService.activeDropletColumn.name, nfRegistryService.activeDropletColumn.sortOrder);"class="push-right-sm"></td-chips>
|
||||
</div>
|
||||
<mat-menu #dropletGridSortMenu="matMenu" [overlapTrigger]="false">
|
||||
<div *ngFor="let column of nfRegistryService.dropletColumns">
|
||||
<button mat-menu-item *ngIf="column.sortable" (click)="nfRegistryService.sortDroplets(column);">
|
||||
|
@ -35,8 +37,17 @@ limitations under the License.
|
|||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
<button [disabled]="nfRegistryService.buckets.length === 0 || (nfRegistryService.filterWritableBuckets(nfRegistryService.buckets)).length === 0"
|
||||
(click)="nfRegistryService.openImportNewFlowDialog(nfRegistryService.buckets, nfRegistryService.bucket)"
|
||||
class="push-left-sm push-top-sm" data-automation-id="import-new-flow-button"
|
||||
color="fds-primary" mat-raised-button i18n="Import new flow button|A button for importing a new flow in the registry.">
|
||||
Import New Flow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="import-new-flow-disabled-message" flex fxLayout="row" fxLayoutAlign="end center" class="pad-right-xxl pad-bottom-sm" *ngIf="(nfRegistryService.filterWritableBuckets(nfRegistryService.buckets)).length === 0 && nfRegistryService.buckets.length != 0">
|
||||
<span>You are not authorized to import flows.</span>
|
||||
</div>
|
||||
<div id="nifi-registry-explorer-grid-list-viewer-droplet-container" class="pad-right-xxl pad-left-xxl"
|
||||
*ngIf="nfRegistryService.filteredDroplets.length > 0">
|
||||
<div *ngFor="let droplet of nfRegistryService.filteredDroplets" [@flyInOut]>
|
||||
|
@ -64,9 +75,9 @@ limitations under the License.
|
|||
<mat-menu class="fds-primary-dropdown-button-menu" #primaryButtonDropdownMenu="matMenu"
|
||||
[overlapTrigger]="false">
|
||||
<button mat-menu-item *ngFor="let action of nfRegistryService.dropletActions"
|
||||
[disabled]="!droplet.permissions.canDelete"
|
||||
[disabled]="action.disabled(droplet)"
|
||||
(click)="nfRegistryService.executeDropletAction(action, droplet)">
|
||||
<span>{{action.name}}</span>
|
||||
{{action.name}}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
@ -118,7 +129,10 @@ limitations under the License.
|
|||
<div class="pad-bottom-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pad-right-xxl pad-left-xxl" *ngIf="nfRegistryService.filteredDroplets.length === 0 && !nfRegistryService.inProgress">
|
||||
<div class="pad-right-xxl pad-left-xxl" *ngIf="nfRegistryService.filteredDroplets.length === 0 && nfRegistryService.buckets.length != 0 && !nfRegistryService.inProgress">
|
||||
<p class="text-center">No results match this query.</p>
|
||||
</div>
|
||||
<div class="pad-right-xxl pad-left-xxl" *ngIf="nfRegistryService.filteredDroplets.length === 0 && nfRegistryService.buckets.length === 0 && !nfRegistryService.inProgress">
|
||||
<p class="text-center">There are no buckets to display.</p>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue