Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into raspberry
Browse files Browse the repository at this point in the history
  • Loading branch information
sfmskywalker committed Jan 16, 2025
2 parents 8777511 + 5478948 commit aaf5336
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 78 deletions.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Elsa.Studio.Workflows.Domain.Services;
using Elsa.Studio.Workflows.Domain.Models;
using Microsoft.AspNetCore.Components.Forms;

namespace Elsa.Studio.Workflows.Domain.Contracts;
Expand All @@ -11,5 +11,5 @@ public interface IWorkflowDefinitionImporter
/// <summary>
/// Imports a set of files containing workflow definitions.
/// </summary>
Task<IEnumerable<IBrowserFile>> ImportFilesAsync(IReadOnlyList<IBrowserFile> files, ImportOptions? options = null);
Task<IEnumerable<WorkflowImportResult>> ImportFilesAsync(IReadOnlyList<IBrowserFile> files, ImportOptions? options = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;

namespace Elsa.Studio.Workflows.Domain.Models;

public class ImportOptions
{
public int MaxAllowedSize { get; set; } = 1024 * 1024 * 10; // 10 MB
public string? DefinitionId { get; set; }
public Func<WorkflowDefinition, Task>? ImportedCallback { get; set; }
public Func<Exception, Task> ErrorCallback { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Elsa.Studio.Workflows.Domain.Models;

public record WorkflowImportFailure(string ErrorMessage, WorkflowImportFailureType FailureType);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Elsa.Studio.Workflows.Domain.Models;

public enum WorkflowImportFailureType
{
Exception,
InvalidSchema
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;

namespace Elsa.Studio.Workflows.Domain.Models;

public class WorkflowImportResult
{
public string FileName { get; set; }
public WorkflowDefinition? WorkflowDefinition { get; set; }
public WorkflowImportFailure? Failure { get; set; }
public bool IsSuccess => Failure == null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Studio.Contracts;
using Elsa.Studio.Workflows.Domain.Contracts;
using Elsa.Studio.Workflows.Domain.Models;
using Elsa.Studio.Workflows.Domain.Notifications;
using Microsoft.AspNetCore.Components.Forms;

Expand All @@ -23,10 +24,10 @@ private async Task<IWorkflowDefinitionsApi> GetApiAsync(CancellationToken cancel
}

/// <inheritdoc />
public async Task<IEnumerable<IBrowserFile>> ImportFilesAsync(IReadOnlyList<IBrowserFile> files, ImportOptions? options = null)
public async Task<IEnumerable<WorkflowImportResult>> ImportFilesAsync(IReadOnlyList<IBrowserFile> files, ImportOptions? options = null)
{
var importedFiles = new List<IBrowserFile>();
var maxAllowedSize = options?.MaxAllowedSize ?? 1024 * 1024 * 10; // 10 MB
var results = new List<WorkflowImportResult>();

foreach (var file in files)
{
Expand All @@ -35,60 +36,63 @@ public async Task<IEnumerable<IBrowserFile>> ImportFilesAsync(IReadOnlyList<IBro

if (file.ContentType == MediaTypeNames.Application.Zip || file.Name.EndsWith(".zip"))
{
var success = await ImportZipFileAsync(stream, options);
if (success)
{
importedFiles.Add(file);
await mediator.NotifyAsync(new ImportedFile(file));
}
var importZipFileResults = await ImportZipFileAsync(stream, options);
results.AddRange(importZipFileResults);
await mediator.NotifyAsync(new ImportedFile(file));
}

else if (file.ContentType == MediaTypeNames.Application.Json || file.Name.EndsWith(".json"))
{
var success = await ImportFromStreamAsync(stream, options);
if (success)
{
importedFiles.Add(file);
await mediator.NotifyAsync(new ImportedFile(file));
}
var importFromStreamResult = await ImportFromStreamAsync(file.Name, stream, options);
results.Add(importFromStreamResult);
await mediator.NotifyAsync(new ImportedFile(file));
}
}

return importedFiles;
return results;
}
private async Task<bool> ImportZipFileAsync(Stream stream, ImportOptions? options)

private async Task<IList<WorkflowImportResult>> ImportZipFileAsync(Stream stream, ImportOptions? options)
{
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
var zipArchive = new ZipArchive(memoryStream);
var hasErrors = false;
var importResultList = new List<WorkflowImportResult>();

foreach (var entry in zipArchive.Entries)
try
{
if (entry.FullName.EndsWith(".json"))
var zipArchive = new ZipArchive(memoryStream);
foreach (var entry in zipArchive.Entries.Where(x => !x.FullName.StartsWith("__MACOSX", StringComparison.OrdinalIgnoreCase)))
{
await using var entryStream = entry.Open();
var success = await ImportFromStreamAsync(entryStream, options);
if (entry.FullName.EndsWith(".json"))
{
await using var entryStream = entry.Open();
var result = await ImportFromStreamAsync(entry.Name, entryStream, options);

if (!success)
hasErrors = true;
importResultList.Add(result);
}
else if (entry.FullName.EndsWith(".zip"))
{
await using var entryStream = entry.Open();
var results = await ImportZipFileAsync(entryStream, options);
importResultList.AddRange(results);
}
}
else if (entry.FullName.EndsWith(".zip"))
}
catch (Exception e)
{
if (options?.ErrorCallback != null)
await options.ErrorCallback(e);
importResultList.Add(new()
{
await using var entryStream = entry.Open();
var success = await ImportZipFileAsync(entryStream, options);

if (!success)
hasErrors = true;
}
Failure = new(e.Message, WorkflowImportFailureType.Exception)
});
}

return !hasErrors;
return importResultList;
}

private async Task<bool> ImportFromStreamAsync(Stream stream, ImportOptions? options)
private async Task<WorkflowImportResult> ImportFromStreamAsync(string fileName, Stream stream, ImportOptions? options)
{
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();
Expand All @@ -97,35 +101,49 @@ private async Task<bool> ImportFromStreamAsync(Stream stream, ImportOptions? opt
try
{
await mediator.NotifyAsync(new ImportingJson(json));

if(!workflowJsonDetector.IsWorkflowSchema(json))
return true;


if (!workflowJsonDetector.IsWorkflowSchema(json))
{
return new()
{
FileName = fileName,
Failure = new("Invalid schema", WorkflowImportFailureType.InvalidSchema)
};
}

var model = JsonSerializer.Deserialize<WorkflowDefinitionModel>(json, jsonSerializerOptions)!;
if(options?.DefinitionId != null)

if (options?.DefinitionId != null)
model.DefinitionId = options.DefinitionId;

await mediator.NotifyAsync(new ImportingWorkflowDefinition(model));
var api = await GetApiAsync();
var newWorkflowDefinition = await api.ImportAsync(model);
if(options?.ImportedCallback != null)

if (options?.ImportedCallback != null)
await options.ImportedCallback(newWorkflowDefinition);

await mediator.NotifyAsync(new ImportedWorkflowDefinition(newWorkflowDefinition));
await mediator.NotifyAsync(new ImportedJson(json));

return new()
{
FileName = fileName,
WorkflowDefinition = newWorkflowDefinition
};
}
catch (Exception e)
{
if(options?.ErrorCallback != null)
if (options?.ErrorCallback != null)
await options.ErrorCallback(e);
return false;
return new()
{
FileName = fileName,
Failure = new(e.Message, WorkflowImportFailureType.Exception)
};
}

return true;
}

private static JsonSerializerOptions CreateJsonSerializerOptions()
{
JsonSerializerOptions options = new()
Expand All @@ -137,12 +155,4 @@ private static JsonSerializerOptions CreateJsonSerializerOptions()
options.Converters.Add(new VersionOptionsJsonConverter());
return options;
}
}

public class ImportOptions
{
public int MaxAllowedSize { get; set; } = 1024 * 1024 * 10; // 10 MB
public string? DefinitionId { get; set; }
public Func<WorkflowDefinition, Task>? ImportedCallback { get; set; }
public Func<Exception, Task> ErrorCallback { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Elsa.Studio.Workflows.Components.WorkflowDefinitionEditor.Components.ActivityProperties;
using Elsa.Studio.Workflows.Domain.Contracts;
using Elsa.Studio.Workflows.Domain.Models;
using Elsa.Studio.Workflows.Domain.Services;
using Elsa.Studio.Workflows.Models;
using Elsa.Studio.Workflows.Shared.Components;
using Elsa.Studio.Workflows.UI.Contracts;
Expand All @@ -23,7 +22,7 @@
using Radzen;
using Radzen.Blazor;
using ThrottleDebounce;
using static MudBlazor.Colors;
using Variant = MudBlazor.Variant;

namespace Elsa.Studio.Workflows.Components.WorkflowDefinitionEditor.Components;

Expand Down Expand Up @@ -57,7 +56,7 @@ public WorkflowEditor()

/// Gets the selected activity ID.
public string? SelectedActivityId { get; private set; }

[Inject] private IWorkflowDefinitionEditorService WorkflowDefinitionEditorService { get; set; } = null!;
[Inject] private IWorkflowDefinitionImporter WorkflowDefinitionImporter { get; set; } = null!;
[Inject] private IActivityVisitor ActivityVisitor { get; set; } = null!;
Expand Down Expand Up @@ -219,7 +218,7 @@ await result.OnFailedAsync(errors =>
}
});
}

private void SelectActivity(JsonObject activity)
{
// Setting the activity to null first and then requesting an update is a workaround to ensure that BlazorMonaco gets destroyed first.
Expand Down Expand Up @@ -368,17 +367,36 @@ private async Task ImportFilesAsync(IReadOnlyList<IBrowserFile> files)
return Task.CompletedTask;
}
};
var importedFiles = (await WorkflowDefinitionImporter.ImportFilesAsync(files, options)).ToList();
var importResults = (await WorkflowDefinitionImporter.ImportFilesAsync(files, options)).ToList();
var failedImports = importResults.Where(x => !x.IsSuccess).ToList();
var successfulImports = importResults.Where(x => x.IsSuccess).ToList();

IsProgressing = false;
_isDirty = false;
StateHasChanged();

if (importedFiles.Count == 0)
Snackbar.Add("No files were imported.", Severity.Warning);
else if (importedFiles.Count == 1)
Snackbar.Add($"Successfully imported workflow definition from file {importedFiles[0].Name}.", Severity.Success);
else if (importedFiles.Count > 1)
Snackbar.Add($"Successfully imported {importedFiles.Count} files.", Severity.Success);
if (importResults.Count == 0)
{
Snackbar.Add("No workflows were imported.", Severity.Info);
return;
}

if (successfulImports.Count == 1)
Snackbar.Add($"Successfully imported 1 workflow definition.", Severity.Success, ConfigureSnackbar);
else if (importResults.Count > 1)
Snackbar.Add($"Successfully imported {importResults.Count} workflow definitions.", Severity.Success, ConfigureSnackbar);

if (failedImports.Count == 1)
Snackbar.Add($"Failed to import 1 workflow definition: {failedImports[0].Failure!.ErrorMessage}", Severity.Error, ConfigureSnackbar);
else if (failedImports.Count > 1)
Snackbar.Add($"Failed to import {failedImports.Count} workflow definitions. Errors: {string.Join(", ", failedImports.Select(x => x.Failure!.ErrorMessage))}", Severity.Error, ConfigureSnackbar);

return;
void ConfigureSnackbar(SnackbarOptions snackbarOptions)
{
snackbarOptions.SnackbarVariant = Variant.Filled;
snackbarOptions.CloseAfterNavigation = failedImports.Count > 0;
snackbarOptions.VisibleStateDuration = failedImports.Count > 0 ? 10000 : 3000;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public partial class WorkflowDefinitionList
private string SearchTerm { get; set; } = string.Empty;
private bool IsReadOnlyMode { get; set; }
private const string ReadonlyWorkflowsExcluded = "The read-only workflows will not be affected.";

private async Task<TableData<WorkflowDefinitionRow>> ServerReload(TableState state, CancellationToken cancellationToken)
{
var request = new ListWorkflowDefinitionsRequest
Expand Down Expand Up @@ -163,12 +163,12 @@ private async Task OnRunWorkflowClicked(WorkflowDefinitionRow workflowDefinition
var definitionId = workflowDefinitionRow!.DefinitionId;
var response = await WorkflowDefinitionService.ExecuteAsync(definitionId, request);

if(response.CannotStart)
if (response.CannotStart)
{
Snackbar.Add("The workflow cannot be started", Severity.Error);
return;
}

Snackbar.Add("Successfully started workflow", Severity.Success);
}

Expand Down Expand Up @@ -324,9 +324,21 @@ private Task OnImportClicked()

private async Task OnFilesSelected(IReadOnlyList<IBrowserFile> files)
{
var importedFiles = (await WorkflowDefinitionImporter.ImportFilesAsync(files)).ToList();
var message = importedFiles.Count == 1 ? "Successfully imported one workflow" : $"Successfully imported {importedFiles.Count} workflows";
Snackbar.Add(message, Severity.Success, options => { options.SnackbarVariant = Variant.Filled; });
var results = (await WorkflowDefinitionImporter.ImportFilesAsync(files)).ToList();
var successfulResultCount = results.Count(x => x.IsSuccess);
var failedResultCount = results.Count(x => !x.IsSuccess);
var successfulWorkflowsTerm = successfulResultCount == 1 ? "workflow" : "workflows";
var failedWorkflowsTerm = failedResultCount == 1 ? "workflow" : "workflows";
var message = results.Count == 0 ? "No workflows found to import." :
successfulResultCount > 0 && failedResultCount == 0 ? $"{successfulResultCount} {successfulWorkflowsTerm} imported successfully." :
successfulResultCount == 0 && failedResultCount > 0 ? $"Failed to import {failedResultCount} {failedWorkflowsTerm}." : $"{successfulResultCount} {successfulWorkflowsTerm} imported successfully. {failedResultCount} {failedWorkflowsTerm} failed to import.";
var severity = results.Count == 0 ? Severity.Info : successfulResultCount > 0 && failedResultCount > 0 ? Severity.Warning : failedResultCount == 0 ? Severity.Success : Severity.Error;
Snackbar.Add(message, severity, options =>
{
options.SnackbarVariant = Variant.Filled;
options.CloseAfterNavigation = failedResultCount > 0;
options.VisibleStateDuration = failedResultCount > 0 ? 10000 : 3000;
});
Reload();
}

Expand Down Expand Up @@ -382,4 +394,4 @@ private record WorkflowDefinitionRow(
string? Description,
bool IsPublished,
bool IsReadOnlyMode);
}
}

0 comments on commit aaf5336

Please sign in to comment.