Implement Export feature

This commit is contained in:
Sipke Schoorstra 2021-03-11 13:17:38 +01:00
parent f61df66148
commit e66b410545
8 changed files with 131 additions and 5 deletions

View file

@ -87,6 +87,7 @@ export namespace Components {
}
interface ElsaWorkflowPublishButton {
"publishing": boolean;
"serverUrl": string;
"workflowDefinition": WorkflowDefinition;
}
interface ElsaWorkflowSettingsModal {
@ -301,6 +302,7 @@ declare namespace LocalJSX {
"onPublishClicked"?: (event: CustomEvent<any>) => void;
"onUnPublishClicked"?: (event: CustomEvent<any>) => void;
"publishing"?: boolean;
"serverUrl"?: string;
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaWorkflowSettingsModal {

View file

@ -1,7 +1,11 @@
import {Component, Host, h, Prop, State, Event, EventEmitter, Watch} from '@stencil/core';
import {enter, leave, toggle} from 'el-transition'
import {registerClickOutside} from "stencil-click-outside";
import {ActivityModel, WorkflowDefinition} from "../../../../models";
import {ActivityModel, VersionOptions, WorkflowDefinition} from "../../../../models";
import {createElsaClient} from "../../../../services/elsa-client";
import Tunnel, {WorkflowEditorState} from '../../../data/workflow-editor';
import {ElsaTextProperty} from "../../properties/elsa-text-property/elsa-text-property";
import {downloadFromBlob, downloadFromText} from "../../../../utils/download";
@Component({
tag: 'elsa-workflow-publish-button',
@ -12,6 +16,7 @@ export class ElsaWorkflowPublishButton {
@Prop() workflowDefinition: WorkflowDefinition;
@Prop() publishing: boolean;
@Prop({attribute: 'server-url', reflect: true}) serverUrl: string;
@Event({bubbles: true}) publishClicked: EventEmitter;
@Event({bubbles: true}) unPublishClicked: EventEmitter;
@ -34,6 +39,16 @@ export class ElsaWorkflowPublishButton {
this.unPublishClicked.emit();
}
async onExportClick(e: Event) {
e.preventDefault();
const client = createElsaClient(this.serverUrl);
const workflowDefinition = this.workflowDefinition;
const versionOptions: VersionOptions = {version: workflowDefinition.version};
const response = await client.workflowDefinitionsApi.export(workflowDefinition.definitionId, versionOptions);
downloadFromBlob(response.data, { contentType: 'application/json', fileName: response.fileName });
}
render() {
return (
<Host ref={el => registerClickOutside(this, el, this.closeMenu)}>
@ -58,7 +73,7 @@ export class ElsaWorkflowPublishButton {
<div class="divide-y divide-gray-100 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="option-menu">
<div class="py-1" role="none">
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">
<a href="#" onClick={e => this.onExportClick(e)} class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">
Export
</a>
@ -114,3 +129,5 @@ export class ElsaWorkflowPublishButton {
);
}
}
Tunnel.injectProps(ElsaWorkflowPublishButton, ['serverUrl']);

View file

@ -28,6 +28,21 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
retract: async workflowDefinitionId => {
const response = await httpClient.post<WorkflowDefinition>(`v1/workflow-definitions/${workflowDefinitionId}/retract`);
return response.data;
},
export: async (workflowDefinitionId, versionOptions): Promise<ExportWorkflowResponse> => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/${versionOptionsString}/export`, null, {
responseType: 'blob'
});
const contentDispositionHeader = response.headers["content-disposition"]; // Only available if the Elsa Server exposes the "Content-Disposition" header.
const fileName = contentDispositionHeader ? contentDispositionHeader.split(";")[1].split("=")[1] : `workflow-definition-${workflowDefinitionId}.json`;
const data = response.data;
return {
fileName: fileName,
data: data
};
}
},
scriptingApi: {
@ -56,6 +71,8 @@ export interface WorkflowDefinitionsApi {
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>
retract(workflowDefinitionId: string): Promise<WorkflowDefinition>
export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise<ExportWorkflowResponse>
}
export interface ScriptingApi {
@ -76,3 +93,8 @@ export interface SaveWorkflowDefinitionRequest {
activities: Array<ActivityDefinition>
connections: Array<ConnectionDefinition>
}
export interface ExportWorkflowResponse {
fileName: string;
data: Blob;
}

View file

@ -0,0 +1,30 @@
export interface DownloadOptions {
fileName?: string;
contentType?: string;
}
export function downloadFromUrl(url: string, options: DownloadOptions) {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = options.fileName !== null && options.fileName !== void 0 ? options.fileName : '';
anchorElement.click();
anchorElement.remove();
}
export function downloadFromBytes(content: Uint8Array, options: DownloadOptions) {
const contentType = options.contentType || 'application/octet-stream';
const base64String = btoa(String.fromCharCode(...content));
const url = `data:${contentType}";base64,${base64String}`;
downloadFromUrl(url, {fileName: options.fileName});
}
export function downloadFromText(content: string, options: DownloadOptions) {
const utf8Encode = new TextEncoder();
const bytes = utf8Encode.encode(content);
downloadFromBytes(bytes, options)
}
export function downloadFromBlob(content: Blob, options: DownloadOptions) {
const url = window.URL.createObjectURL(content);
downloadFromUrl(url, options)
}

View file

@ -1,4 +1,5 @@
using System;
using System.Net.Http.Headers;
using Elsa;
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Elsa.Persistence.EntityFramework.Sqlite;
@ -57,7 +58,7 @@ namespace ElsaDashboard.Samples.Monolith
// Allow arbitrary client browser apps to access the API for demo purposes only.
// In a production environment, make sure to allow only origins you trust.
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("Content-Disposition")));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View file

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj" />
<ProjectReference Include="..\..\..\dashboards\blazor\ElsaDashboard.Application\ElsaDashboard.Application.csproj" />
<ProjectReference Include="..\..\..\dashboards\blazor\ElsaDashboard.Backend\ElsaDashboard.Backend.csproj" />
<ProjectReference Include="..\ElsaDashboard.Samples.WebAssembly\ElsaDashboard.Samples.WebAssembly.csproj" />

View file

@ -36,10 +36,10 @@ namespace Elsa.Samples.Server.Host
services
.AddElsaApiEndpoints()
.AddElsaSwagger();
// Allow arbitrary client browser apps to access the API for demo purposes only.
// In a production environment, make sure to allow only origins you trust.
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("Content-Disposition")));
}
public void Configure(IApplicationBuilder app)
@ -54,6 +54,7 @@ namespace Elsa.Samples.Server.Host
app
.UseCors()
.UseHttpActivities()
.UseCors()
.UseRouting()
.UseEndpoints(configure => configure.MapControllers());
}

View file

@ -0,0 +1,52 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
{
[ApiController]
[ApiVersion("1")]
[Route("v{apiVersion:apiVersion}/workflow-definitions/{workflowDefinitionId}/{versionOptions}/export")]
[Produces("application/json")]
public class Export : ControllerBase
{
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly IContentSerializer _contentSerializer;
public Export(IWorkflowDefinitionStore workflowDefinitionStore, IContentSerializer contentSerializer)
{
_workflowDefinitionStore = workflowDefinitionStore;
_contentSerializer = contentSerializer;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation(
Summary = "Exports the specified workflow definition as a JSON download.",
Description = "Exports the specified workflow definition as a JSON download.",
OperationId = "WorkflowDefinitions.Export",
Tags = new[] { "WorkflowDefinitions" })
]
public async Task<IActionResult> Handle(string workflowDefinitionId, VersionOptions versionOptions, ApiVersion apiVersion, CancellationToken cancellationToken)
{
var workflowDefinition = await _workflowDefinitionStore.FindByDefinitionIdAsync(workflowDefinitionId, versionOptions, cancellationToken);
if (workflowDefinition == null)
return NotFound();
var json = _contentSerializer.Serialize(workflowDefinition);
var bytes = Encoding.UTF8.GetBytes(json);
var workflowName = workflowDefinition.Name is null or "" ? workflowDefinition.DefinitionId : workflowDefinition.Name.Trim();
var fileName = $"workflow-definition-{workflowName}.json";
return File(bytes, "application/json", fileName);
}
}
}