Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bulk Export/Import of Workflow Definitions #83

Merged
merged 4 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
<PackageReference Include="ThrottleDebounce" Version="2.0.0"/>
</ItemGroup>

<!-- <ItemGroup Label="Elsa">-->
<!-- <ProjectReference Include="..\..\..\..\..\elsa-core\v3\src\clients\Elsa.Api.Client\Elsa.Api.Client.csproj"/>-->
<!-- </ItemGroup>-->
<!-- <ItemGroup Label="Elsa">-->
<!-- <ProjectReference Include="..\..\..\..\..\elsa-core\v3\src\clients\Elsa.Api.Client\Elsa.Api.Client.csproj"/>-->
<!-- </ItemGroup>-->

<ItemGroup Label="Elsa">
<PackageReference Include="Elsa.Api.Client" Version="3.0.0-preview.843"/>
<PackageReference Include="Elsa.Api.Client" Version="3.0.0-preview.845"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Elsa.Api.Client.Shared.Models;
using Elsa.Studio.Models;
using Elsa.Studio.Workflows.Domain.Models;
using Refit;

namespace Elsa.Studio.Workflows.Domain.Contracts;

Expand Down Expand Up @@ -102,6 +103,11 @@ public interface IWorkflowDefinitionService
/// </summary>
Task<WorkflowDefinition> ImportDefinitionAsync(WorkflowDefinitionModel definitionModel, CancellationToken cancellationToken = default);

/// <summary>
/// Exports a set of workflow definitions.
/// </summary>
Task<FileDownload> BulkExportDefinitionsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default);

/// <summary>
/// Updates the references of a workflow definition.
/// </summary>
Expand All @@ -116,4 +122,9 @@ public interface IWorkflowDefinitionService
/// Executes a workflow definition.
/// </summary>
Task<string> ExecuteAsync(string definitionId, ExecuteWorkflowDefinitionRequest? request, CancellationToken cancellationToken = default);

/// <summary>
/// Imports a set of files containing workflow definitions.
/// </summary>
Task<int> ImportFilesAsync(IEnumerable<StreamPart> streamParts, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ namespace Elsa.Studio.Workflows.Domain.Models;
/// <summary>
/// Represents a file download.
/// </summary>
public record FileDownload(string? FileName, Stream Content);
public record FileDownload(string FileName, Stream Content);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using System.Text.Json.Nodes;
using Elsa.Api.Client.Extensions;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Requests;
Expand Down Expand Up @@ -249,15 +250,7 @@ public async Task<FileDownload> ExportDefinitionAsync(string definitionId, Versi
{
var api = await GetApiAsync(cancellationToken);
var response = await api.ExportAsync(definitionId, versionOptions, cancellationToken);
var fileName = $"workflow-definition-{definitionId}.json";

if (response.Headers.TryGetValues("content-disposition", out var contentDispositionHeader)) // Only available if the Elsa Server exposes the "Content-Disposition" header.
{
var values = contentDispositionHeader?.ToList() ?? new List<string>();

if (values.Count >= 2)
fileName = values[1].Split('=')[1];
}
var fileName = response.GetDownloadedFileNameOrDefault($"workflow-definition-{definitionId}.json");

return new FileDownload(fileName, response.Content!);
}
Expand All @@ -269,6 +262,16 @@ public async Task<WorkflowDefinition> ImportDefinitionAsync(WorkflowDefinitionMo
return await api.ImportAsync(definitionModel, cancellationToken);
}

/// <inheritdoc />
public async Task<FileDownload> BulkExportDefinitionsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
var api = await GetApiAsync(cancellationToken);
var request = new BulkExportWorkflowDefinitionsRequest(ids.ToArray());
var response = await api.BulkExportAsync(request, cancellationToken);
var fileName = response.GetDownloadedFileNameOrDefault("workflow-definitions.zip");
return new FileDownload(fileName, response.Content!);
}

/// <inheritdoc />
public async Task<UpdateConsumingWorkflowReferencesResponse> UpdateReferencesAsync(string definitionId, CancellationToken cancellationToken = default)
{
Expand All @@ -288,10 +291,17 @@ public async Task<string> ExecuteAsync(string definitionId, ExecuteWorkflowDefin
{
var api = await GetApiAsync(cancellationToken);
var response = await api.ExecuteAsync(definitionId, request, cancellationToken);

var workflowInstanceId = response.Headers.GetValues("x-elsa-workflow-instance-id").First();
return workflowInstanceId;
}

/// <inheritdoc />
public async Task<int> ImportFilesAsync(IEnumerable<StreamPart> streamParts, CancellationToken cancellationToken = default)
{
var api = await GetApiAsync(cancellationToken);
var response = await api.ImportFilesAsync(streamParts.ToList(), cancellationToken);
return response.Count;
}

private async Task<IWorkflowDefinitionsApi> GetApiAsync(CancellationToken cancellationToken = default) => await _remoteBackendApiClientProvider.GetApiAsync<IWorkflowDefinitionsApi>(cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,44 @@

<RadzenSplitter Orientation="Orientation.Vertical" Style="height: calc(100vh - var(--mud-appbar-height));" Resize="@OnResize">
<RadzenSplitterPane Size="70%">
<DiagramDesignerWrapper @ref="_diagramDesigner" Activity="WorkflowDefinition?.Root" IsProgressing="_isProgressing" ActivitySelected="OnActivitySelected" GraphUpdated="OnGraphUpdated">
<DiagramDesignerWrapper @ref="_diagramDesigner" Activity="WorkflowDefinition?.Root" IsProgressing="_isProgressing" ActivitySelected="@OnActivitySelected" GraphUpdated="@OnGraphUpdated">
<CustomToolbarItems>
<MudSwitch T="bool?" Checked="@_autoSave" Color="Color.Primary" Label="Auto-save" LabelPosition="LabelPosition.Start" CheckedChanged="OnAutoSaveChanged"/>
<MudSwitch T="bool?" Checked="@_autoSave" Color="Color.Primary" Label="Auto-save" LabelPosition="LabelPosition.Start" CheckedChanged="@OnAutoSaveChanged"/>
<MudTooltip Text="Save" Delay="1000">
<MudBadge Color="@(_isDirty ? Color.Warning : Color.Success)" Dot="true" Overlap="true" Origin="Origin.BottomRight" Bordered="true" Class="elsa-toolbar-icon">
<MudIconButton Icon="@Icons.Material.Outlined.Save" Title="Save" OnClick="OnSaveClick"/>
<MudIconButton Icon="@Icons.Material.Outlined.Save" Title="Save" OnClick="@OnSaveClick"/>
</MudBadge>
</MudTooltip>

<div id="workflow-file-upload-button-wrapper" class="d-none">
<MudFileUpload T="IBrowserFile" FilesChanged="OnFileSelected"/>
<MudFileUpload T="IBrowserFile" FilesChanged="@OnFileSelected"/>
</div>

<MudTooltip Text="Publish the current workflow." Delay="1000">
<MudButtonGroup Color="Color.Default" Variant="Variant.Text" DisableElevation="true">
<MudIconButton Icon="@Icons.Material.Filled.CloudUpload" Color="Color.Primary" Title="Publish" OnClick="OnPublishClicked"/>
<MudIconButton Icon="@Icons.Material.Filled.CloudUpload" Color="Color.Primary" Title="Publish" OnClick="@OnPublishClicked"/>
<MudMenu Icon="@Icons.Material.Filled.ArrowDropDown">
@if (WorkflowDefinition?.IsPublished == true)
{
<MudTooltip Text="Unpublish the current workflow." Inline="false" Delay="1000">
<MudMenuItem Icon="@Icons.Material.Filled.CloudDownload" OnClick="OnRetractClicked">Unpublish</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.CloudDownload" OnClick="@OnRetractClicked">Unpublish</MudMenuItem>
</MudTooltip>
<MudDivider DividerType="DividerType.FullWidth"></MudDivider>
}
<MudTooltip Text="Download the current workflow as a JSON file." Inline="false" Delay="1000">
<MudMenuItem Icon="@Icons.Material.Filled.FileDownload" OnClick="OnDownloadClicked">Export</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.FileDownload" OnClick="@OnDownloadClicked">Export</MudMenuItem>
</MudTooltip>
<MudTooltip Text="Upload a JSON file containing workflow data to override the current workflow's contents." Inline="false" Delay="1000">
<MudMenuItem Icon="@Icons.Material.Filled.FileUpload" OnClick="OnUploadClicked">Import</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.FileUpload" OnClick="@OnUploadClicked">Import</MudMenuItem>
</MudTooltip>
</MudMenu>
</MudButtonGroup>
</MudTooltip>
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow" Color="Color.Success" Variant="Variant.Text" Title="Run workflow" Class="ml-4" OnClick="OnRunWorkflowClicked"/>
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow" Color="Color.Success" Variant="Variant.Text" Title="Run workflow" Class="ml-4" OnClick="@OnRunWorkflowClicked"/>
</CustomToolbarItems>
</DiagramDesignerWrapper>
</RadzenSplitterPane>
<RadzenSplitterPane Size="30%" @ref="ActivityPropertiesPane">
<ActivityPropertiesPanel @ref="ActivityPropertiesPanel" WorkflowDefinition="@WorkflowDefinition" Activity="@SelectedActivity" ActivityDescriptor="@ActivityDescriptor" OnActivityUpdated="OnSelectedActivityUpdated" VisiblePaneHeight="@_activityPropertiesPaneHeight"/>
<ActivityPropertiesPanel @ref="ActivityPropertiesPanel" WorkflowDefinition="@WorkflowDefinition" Activity="@SelectedActivity" ActivityDescriptor="@ActivityDescriptor" OnActivityUpdated="@OnSelectedActivityUpdated" VisiblePaneHeight="@_activityPropertiesPaneHeight"/>
</RadzenSplitterPane>
</RadzenSplitter>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

<MudContainer MaxWidth="MaxWidth.False">
<PageHeading Text="Workflow definitions"/>

<div id="workflow-file-upload-button-wrapper" class="d-none">
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="@OnFilesSelected"/>
</div>

<MudTable
@ref="_table"
Expand All @@ -21,12 +25,22 @@
@bind-SelectedItems="_selectedRows">
<ToolBarContent>
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Bulk actions" Color="Color.Default" Variant="Variant.Filled">
<MudMenuItem OnClick="OnBulkDeleteClicked">Delete</MudMenuItem>
<MudMenuItem OnClick="OnBulkPublishClicked">Publish</MudMenuItem>
<MudMenuItem OnClick="OnBulkRetractClicked">Unpublish</MudMenuItem>
<MudMenuItem OnClick="@OnBulkDeleteClicked">Delete</MudMenuItem>
<MudMenuItem OnClick="@OnBulkPublishClicked">Publish</MudMenuItem>
<MudMenuItem OnClick="@OnBulkRetractClicked">Unpublish</MudMenuItem>
<MudMenuItem OnClick="@OnBulkExportClicked">Export</MudMenuItem>
</MudMenu>
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@OnCreateWorkflowClicked">Create workflow</MudButton>

<MudButtonGroup Color="Color.Primary" Variant="Variant.Filled" DisableElevation="false">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@OnCreateWorkflowClicked">Create workflow</MudButton>
<MudMenu Icon="@Icons.Material.Filled.ArrowDropDown">
<MudTooltip Text="Upload JSON and/or ZIP files containing workflow data to import." Inline="false" Delay="1000">
<MudMenuItem Icon="@Icons.Material.Filled.FileUpload" OnClick="@OnImportClicked">Import</MudMenuItem>
</MudTooltip>
</MudMenu>
</MudButtonGroup>

</ToolBarContent>
<HeaderContent>
<MudTh>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Api.Client.Converters;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses;
using Elsa.Api.Client.Shared.Models;
using Elsa.Studio.DomInterop.Contracts;
using Elsa.Studio.Workflows.Domain.Contracts;
using Elsa.Studio.Workflows.Models;
using Humanizer;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor;
using Refit;

namespace Elsa.Studio.Workflows.Pages.WorkflowDefinitions.List;

Expand All @@ -24,6 +30,7 @@ public partial class Index
[Inject] private ISnackbar Snackbar { get; set; } = default!;
[Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!;
[Inject] private IFiles Files { get; set; } = default!;
[Inject] private IDomAccessor DomAccessor { get; set; } = default!;

private async Task<TableData<WorkflowDefinitionRow>> ServerReload(TableState state)
{
Expand Down Expand Up @@ -53,7 +60,7 @@ private async Task<TableData<WorkflowDefinitionRow>> ServerReload(TableState sta
: publishedWorkflowDefinitions.Items.FirstOrDefault(x => x.DefinitionId == definition.DefinitionId);
var publishedVersionNumber = publishedVersion?.Version;

return new WorkflowDefinitionRow(definition.DefinitionId, latestVersionNumber, publishedVersionNumber, definition.Name, definition.Description, definition.IsPublished);
return new WorkflowDefinitionRow(definition.Id, definition.DefinitionId, latestVersionNumber, publishedVersionNumber, definition.Name, definition.Description, definition.IsPublished);
})
.ToList();

Expand Down Expand Up @@ -95,7 +102,7 @@ private void Edit(string definitionId)
{
NavigationManager.NavigateTo($"workflows/definitions/{definitionId}/edit");
}

private void Reload()
{
_table.ReloadServerData();
Expand Down Expand Up @@ -173,7 +180,7 @@ private async Task OnBulkPublishClicked()

Reload();
}

private async Task OnBulkRetractClicked()
{
var result = await DialogService.ShowMessageBox("Unpublish selected workflows?", "Are you sure you want to unpublish the selected workflows?", yesText: "Unpublish", cancelText: "Cancel");
Expand Down Expand Up @@ -205,6 +212,28 @@ private async Task OnBulkRetractClicked()
Reload();
}

private async Task OnBulkExportClicked()
{
var workflowVersionIds = _selectedRows.Select(x => x.Id).ToList();
var download = await WorkflowDefinitionService.BulkExportDefinitionsAsync(workflowVersionIds);
var fileName = download.FileName;
await Files.DownloadFileFromStreamAsync(fileName, download.Content);
}

private Task OnImportClicked()
{
return DomAccessor.ClickElementAsync("#workflow-file-upload-button-wrapper input[type=file]");
}

private async Task OnFilesSelected(IReadOnlyList<IBrowserFile> files)
{
var streamParts = files.Select(x => new StreamPart(x.OpenReadStream(), x.Name, x.ContentType)).ToList();
var count = await WorkflowDefinitionService.ImportFilesAsync(streamParts);
var message = count == 1 ? "Successfully imported one workflow" : $"Successfully imported {count} workflows";
Snackbar.Add(message, Severity.Success, options => { options.SnackbarVariant = Variant.Filled; });
Reload();
}

private void OnSearch(string text)
{
_searchString = text;
Expand All @@ -222,6 +251,6 @@ private async Task OnRetractClicked(string definitionId)
await WorkflowDefinitionService.RetractAsync(definitionId);
Snackbar.Add("Workflow retracted", Severity.Success, options => { options.SnackbarVariant = Variant.Filled; });
}
private record WorkflowDefinitionRow(string DefinitionId, int LatestVersion, int? PublishedVersion, string? Name, string? Description, bool IsPublished);

private record WorkflowDefinitionRow(string Id, string DefinitionId, int LatestVersion, int? PublishedVersion, string? Name, string? Description, bool IsPublished);
}