feat: Aggiunto sistema completo di gestione profili per Data Coupler
- Creata nuova libreria Components con componenti Blazor riutilizzabili * ProfileSelector: dropdown per selezione profili salvati * ProfileSaver: componente per salvare configurazioni correnti come profili * ProfileManagement: modale per gestione profili salvati * ProfileQuickActions: bottoni azioni rapide per operazioni sui profili - Esteso CredentialManager con entità e servizi per DataCouplerProfile * Aggiunto modello DataCouplerProfile con configurazioni mapping e metadati * Implementata migrazione Entity Framework per memorizzazione profili * Creato DataCouplerProfileService per operazioni CRUD * Aggiunto CredentialDbContextFactory per operazioni database design-time - Migliorato componente principale DataCoupler con integrazione profili * Integrata funzionalità caricamento/salvataggio profili * Aggiunto selettore profili nella parte superiore dell'interfaccia * Mantenuta retrocompatibilità con funzionalità esistenti * Migliorata esperienza utente con gestione configurazioni salvate - Aggiornata struttura progetto e dipendenze * Aggiunto progetto Components alla soluzione * Aggiornati riferimenti progetti e import * Rimosso progetto obsoleto TestDatabaseFix Questo aggiornamento migliora significativamente il flusso di lavoro permettendo agli utenti di salvare, caricare e gestire configurazioni complete di accoppiamento dati come
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,180 @@
|
||||
@* Componente per la gestione completa dei profili *@
|
||||
|
||||
<!-- Modal per la gestione profili -->
|
||||
@if (ShowModal)
|
||||
{
|
||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-cogs"></i> Gestione Profili
|
||||
</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Caricamento...</span>
|
||||
</div>
|
||||
<p class="mt-2">Caricamento profili...</p>
|
||||
</div>
|
||||
}
|
||||
else if (Profiles == null || !Profiles.Any())
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i>
|
||||
<h6 class="text-muted">Nessun profilo salvato</h6>
|
||||
<p class="text-muted">Configura una connessione e salva il tuo primo profilo!</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Filtro di ricerca -->
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" @bind="SearchTerm" @oninput="FilterProfiles"
|
||||
placeholder="Cerca profili per nome o descrizione..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista profili -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Fonte → Destinazione</th>
|
||||
<th>Creato</th>
|
||||
<th>Ultimo Uso</th>
|
||||
<th width="120">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var profile in GetFilteredProfiles())
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@profile.Name</strong>
|
||||
@if (!string.IsNullOrEmpty(profile.Description))
|
||||
{
|
||||
<br><small class="text-muted">@profile.Description</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary me-1">@GetTypeLabel(profile.SourceType)</span>
|
||||
<i class="fas fa-arrow-right text-muted"></i>
|
||||
<span class="badge bg-success ms-1">@GetTypeLabel(profile.DestinationType)</span>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
@GetProfileSummary(profile)
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
@profile.CreatedAt.ToString("dd/MM/yyyy HH:mm")
|
||||
@if (!string.IsNullOrEmpty(profile.CreatedBy))
|
||||
{
|
||||
<br><span class="text-muted">da @profile.CreatedBy</span>
|
||||
}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
@(profile.LastUsedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai utilizzato")
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
@onclick="() => LoadProfile(profile)"
|
||||
title="Carica questo profilo">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
@onclick="() => ConfirmDelete(profile)"
|
||||
title="Elimina questo profilo">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!GetFilteredProfiles().Any())
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-search text-muted"></i>
|
||||
<p class="text-muted mb-0">Nessun profilo corrisponde alla ricerca</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Message))
|
||||
{
|
||||
<div class="alert alert-@(MessageType) mt-3">
|
||||
<i class="fas fa-@(MessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
|
||||
@Message
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CloseModal">
|
||||
<i class="fas fa-times"></i> Chiudi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Modal conferma eliminazione -->
|
||||
@if (ShowDeleteConfirm && ProfileToDelete != null)
|
||||
{
|
||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.7);">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-danger">
|
||||
<i class="fas fa-exclamation-triangle"></i> Conferma Eliminazione
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Sei sicuro di voler eliminare il profilo <strong>"@ProfileToDelete.Name"</strong>?</p>
|
||||
@if (!string.IsNullOrEmpty(ProfileToDelete.Description))
|
||||
{
|
||||
<p class="text-muted">@ProfileToDelete.Description</p>
|
||||
}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Attenzione:</strong> Questa operazione non può essere annullata.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CancelDelete">
|
||||
<i class="fas fa-times"></i> Annulla
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" @onclick="DeleteProfile" disabled="@IsDeleting">
|
||||
@if (IsDeleting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-trash"></i>
|
||||
}
|
||||
Elimina
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using CredentialManager.Models;
|
||||
|
||||
namespace Components;
|
||||
|
||||
public partial class ProfileManagement
|
||||
{
|
||||
[Parameter] public bool ShowModal { get; set; }
|
||||
[Parameter] public List<DataCouplerProfile>? Profiles { get; set; }
|
||||
[Parameter] public EventCallback OnCloseModal { get; set; }
|
||||
[Parameter] public EventCallback<DataCouplerProfile> OnProfileLoaded { get; set; }
|
||||
[Parameter] public EventCallback<int> OnProfileDeleted { get; set; }
|
||||
[Parameter] public bool IsLoading { get; set; }
|
||||
|
||||
private string SearchTerm { get; set; } = "";
|
||||
private string Message { get; set; } = "";
|
||||
private string MessageType { get; set; } = "info";
|
||||
private bool ShowDeleteConfirm { get; set; } = false;
|
||||
private bool IsDeleting { get; set; } = false;
|
||||
private DataCouplerProfile? ProfileToDelete { get; set; }
|
||||
|
||||
private void FilterProfiles(ChangeEventArgs e)
|
||||
{
|
||||
SearchTerm = e.Value?.ToString() ?? "";
|
||||
}
|
||||
|
||||
private IEnumerable<DataCouplerProfile> GetFilteredProfiles()
|
||||
{
|
||||
if (Profiles == null)
|
||||
return Enumerable.Empty<DataCouplerProfile>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SearchTerm))
|
||||
return Profiles;
|
||||
|
||||
var searchLower = SearchTerm.ToLower();
|
||||
return Profiles.Where(p =>
|
||||
p.Name.ToLower().Contains(searchLower) ||
|
||||
(!string.IsNullOrEmpty(p.Description) && p.Description.ToLower().Contains(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
private string GetTypeLabel(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"database" => "DB",
|
||||
"file" => "File",
|
||||
"rest" => "REST",
|
||||
_ => type.ToUpper()
|
||||
};
|
||||
}
|
||||
|
||||
private string GetProfileSummary(DataCouplerProfile profile)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
// Fonte
|
||||
if (!string.IsNullOrEmpty(profile.SourceTable))
|
||||
parts.Add($"da {profile.SourceTable}");
|
||||
else if (!string.IsNullOrEmpty(profile.SourceFilePath))
|
||||
parts.Add($"da {Path.GetFileName(profile.SourceFilePath)}");
|
||||
|
||||
// Destinazione
|
||||
if (!string.IsNullOrEmpty(profile.DestinationTable))
|
||||
parts.Add($"verso {profile.DestinationTable}");
|
||||
else if (!string.IsNullOrEmpty(profile.DestinationEndpoint))
|
||||
parts.Add($"verso {profile.DestinationEndpoint}");
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private async Task CloseModal()
|
||||
{
|
||||
SearchTerm = "";
|
||||
Message = "";
|
||||
await OnCloseModal.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task LoadProfile(DataCouplerProfile profile)
|
||||
{
|
||||
Message = $"Caricamento profilo '{profile.Name}'...";
|
||||
MessageType = "info";
|
||||
|
||||
await OnProfileLoaded.InvokeAsync(profile);
|
||||
|
||||
Message = $"Profilo '{profile.Name}' caricato con successo!";
|
||||
MessageType = "success";
|
||||
|
||||
// Chiudi il modal dopo un breve delay
|
||||
await Task.Delay(1000);
|
||||
await CloseModal();
|
||||
}
|
||||
|
||||
private void ConfirmDelete(DataCouplerProfile profile)
|
||||
{
|
||||
ProfileToDelete = profile;
|
||||
ShowDeleteConfirm = true;
|
||||
Message = "";
|
||||
}
|
||||
|
||||
private void CancelDelete()
|
||||
{
|
||||
ProfileToDelete = null;
|
||||
ShowDeleteConfirm = false;
|
||||
}
|
||||
|
||||
private async Task DeleteProfile()
|
||||
{
|
||||
if (ProfileToDelete == null)
|
||||
return;
|
||||
|
||||
IsDeleting = true;
|
||||
|
||||
try
|
||||
{
|
||||
await OnProfileDeleted.InvokeAsync(ProfileToDelete.Id);
|
||||
|
||||
Message = $"Profilo '{ProfileToDelete.Name}' eliminato con successo.";
|
||||
MessageType = "success";
|
||||
|
||||
ShowDeleteConfirm = false;
|
||||
ProfileToDelete = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Message = $"Errore nell'eliminazione: {ex.Message}";
|
||||
MessageType = "danger";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsDeleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMessage(string message, string type = "info")
|
||||
{
|
||||
Message = message;
|
||||
MessageType = type;
|
||||
}
|
||||
|
||||
public void ClearMessage()
|
||||
{
|
||||
Message = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
@* Componente per salvare la configurazione corrente come profilo *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-save"></i> Salva Configurazione Corrente
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!ShowSaveForm)
|
||||
{
|
||||
<button type="button" class="btn btn-success" @onclick="ShowSaveDialog" disabled="@(!CanSave)">
|
||||
<i class="fas fa-plus"></i> Salva come Nuovo Profilo
|
||||
</button>
|
||||
@if (!CanSave)
|
||||
{
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Configura fonte e destinazione per abilitare il salvataggio
|
||||
</small>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="ProfileData" OnValidSubmit="SaveProfile">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome Profilo <span class="text-danger">*</span></label>
|
||||
<InputText @bind-Value="ProfileData.Name" class="form-control"
|
||||
placeholder="Es: Export Clienti a CRM" />
|
||||
<ValidationMessage For="@(() => ProfileData.Name)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descrizione</label>
|
||||
<InputText @bind-Value="ProfileData.Description" class="form-control"
|
||||
placeholder="Descrizione opzionale del profilo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(SaveMessage))
|
||||
{
|
||||
<div class="alert alert-@(SaveMessageType) mb-3">
|
||||
<i class="fas fa-@(SaveMessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
|
||||
@SaveMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Anteprima Configurazione -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Configurazione da salvare:</label>
|
||||
<div class="bg-light p-3 rounded small">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>Fonte:</strong> @GetSourceSummary()<br />
|
||||
@if (!string.IsNullOrEmpty(SourceSchema))
|
||||
{
|
||||
<span class="text-muted">Schema: @SourceSchema</span><br />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(SourceTable))
|
||||
{
|
||||
<span class="text-muted">Tabella: @SourceTable</span>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Destinazione:</strong> @GetDestinationSummary()<br />
|
||||
@if (!string.IsNullOrEmpty(DestinationSchema))
|
||||
{
|
||||
<span class="text-muted">Schema: @DestinationSchema</span><br />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(DestinationTable))
|
||||
{
|
||||
<span class="text-muted">Tabella: @DestinationTable</span>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(DestinationEndpoint))
|
||||
{
|
||||
<span class="text-muted">Endpoint: @DestinationEndpoint</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (FieldMappings != null && FieldMappings.Any())
|
||||
{
|
||||
<hr class="my-2" />
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
@FieldMappings.Count mapping dei campi configurati
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
<button type="button" class="btn btn-secondary me-2" @onclick="CancelSave">
|
||||
<i class="fas fa-times"></i> Annulla
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" disabled="@IsSaving">
|
||||
@if (IsSaving)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-save"></i>
|
||||
}
|
||||
Salva Profilo
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using CredentialManager.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Components;
|
||||
|
||||
public partial class ProfileSaver
|
||||
{
|
||||
[Parameter] public bool CanSave { get; set; }
|
||||
[Parameter] public string SourceType { get; set; } = "";
|
||||
[Parameter] public int? SourceCredentialId { get; set; }
|
||||
[Parameter] public string? SourceSchema { get; set; }
|
||||
[Parameter] public string? SourceTable { get; set; }
|
||||
[Parameter] public string? SourceFilePath { get; set; }
|
||||
[Parameter] public string DestinationType { get; set; } = "";
|
||||
[Parameter] public int? DestinationCredentialId { get; set; }
|
||||
[Parameter] public string? DestinationSchema { get; set; }
|
||||
[Parameter] public string? DestinationTable { get; set; }
|
||||
[Parameter] public string? DestinationEndpoint { get; set; }
|
||||
[Parameter] public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||
[Parameter] public EventCallback<DataCouplerProfileDto> OnProfileSaved { get; set; }
|
||||
|
||||
private bool ShowSaveForm { get; set; } = false;
|
||||
private bool IsSaving { get; set; } = false;
|
||||
private string SaveMessage { get; set; } = "";
|
||||
private string SaveMessageType { get; set; } = "info";
|
||||
private ProfileFormModel ProfileData { get; set; } = new();
|
||||
|
||||
private void ShowSaveDialog()
|
||||
{
|
||||
ProfileData = new ProfileFormModel();
|
||||
ShowSaveForm = true;
|
||||
SaveMessage = "";
|
||||
}
|
||||
|
||||
private void CancelSave()
|
||||
{
|
||||
ShowSaveForm = false;
|
||||
SaveMessage = "";
|
||||
ProfileData = new();
|
||||
}
|
||||
|
||||
private async Task SaveProfile()
|
||||
{
|
||||
IsSaving = true;
|
||||
SaveMessage = "";
|
||||
|
||||
try
|
||||
{
|
||||
var profileDto = new DataCouplerProfileDto
|
||||
{
|
||||
Name = ProfileData.Name,
|
||||
Description = ProfileData.Description,
|
||||
SourceType = SourceType,
|
||||
SourceCredentialId = SourceCredentialId,
|
||||
SourceSchema = SourceSchema,
|
||||
SourceTable = SourceTable,
|
||||
SourceFilePath = SourceFilePath,
|
||||
DestinationType = DestinationType,
|
||||
DestinationCredentialId = DestinationCredentialId,
|
||||
DestinationSchema = DestinationSchema,
|
||||
DestinationTable = DestinationTable,
|
||||
DestinationEndpoint = DestinationEndpoint,
|
||||
FieldMappings = FieldMappings
|
||||
};
|
||||
|
||||
await OnProfileSaved.InvokeAsync(profileDto);
|
||||
|
||||
SaveMessage = $"Profilo '{ProfileData.Name}' salvato con successo!";
|
||||
SaveMessageType = "success";
|
||||
|
||||
// Reset form after successful save
|
||||
await Task.Delay(1500); // Show success message briefly
|
||||
ShowSaveForm = false;
|
||||
ProfileData = new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SaveMessage = $"Errore nel salvataggio: {ex.Message}";
|
||||
SaveMessageType = "danger";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSourceSummary()
|
||||
{
|
||||
return SourceType switch
|
||||
{
|
||||
"database" => "Database",
|
||||
"file" => "File Excel/CSV",
|
||||
_ => "Non configurato"
|
||||
};
|
||||
}
|
||||
|
||||
private string GetDestinationSummary()
|
||||
{
|
||||
return DestinationType switch
|
||||
{
|
||||
"database" => "Database",
|
||||
"rest" => "REST API",
|
||||
_ => "Non configurato"
|
||||
};
|
||||
}
|
||||
|
||||
public void SetMessage(string message, string type = "info")
|
||||
{
|
||||
SaveMessage = message;
|
||||
SaveMessageType = type;
|
||||
}
|
||||
|
||||
public class ProfileFormModel
|
||||
{
|
||||
[Required(ErrorMessage = "Il nome del profilo è obbligatorio")]
|
||||
[StringLength(100, ErrorMessage = "Il nome non può superare i 100 caratteri")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[StringLength(500, ErrorMessage = "La descrizione non può superare i 500 caratteri")]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@* Componente per la selezione e caricamento dei profili *@
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-user-cog"></i> Gestione Profili
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- Selezione Profilo Esistente -->
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Carica Profilo Salvato:</label>
|
||||
<div class="input-group">
|
||||
<select class="form-select" @onchange="OnProfileSelected">
|
||||
<option value="">-- Seleziona un profilo --</option>
|
||||
@if (Profiles != null)
|
||||
{
|
||||
@foreach (var profile in Profiles)
|
||||
{
|
||||
<option value="@profile.Id" selected="@(SelectedProfileId == profile.Id)">
|
||||
@profile.Name @if (!string.IsNullOrEmpty(profile.Description)) { <span class="text-muted">- @profile.Description</span> }
|
||||
</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
<button type="button" class="btn btn-outline-primary" @onclick="LoadSelectedProfile"
|
||||
disabled="@(SelectedProfileId == 0 || IsLoading)">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fas fa-download"></i>
|
||||
}
|
||||
Carica
|
||||
</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(LoadMessage))
|
||||
{
|
||||
<div class="alert alert-@(LoadMessageType) alert-sm mt-2 mb-0">
|
||||
<i class="fas fa-@(LoadMessageType == "success" ? "check-circle" : "exclamation-triangle")"></i>
|
||||
@LoadMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pulsante Gestione Profili -->
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-secondary w-100" @onclick="OpenProfileManagement">
|
||||
<i class="fas fa-cogs"></i> Gestisci Profili
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using CredentialManager.Models;
|
||||
|
||||
namespace Components;
|
||||
|
||||
public partial class ProfileSelector
|
||||
{
|
||||
[Parameter] public List<DataCouplerProfile>? Profiles { get; set; }
|
||||
[Parameter] public EventCallback<DataCouplerProfile> OnProfileLoaded { get; set; }
|
||||
[Parameter] public EventCallback OnManageProfiles { get; set; }
|
||||
[Parameter] public bool IsLoading { get; set; }
|
||||
|
||||
private int SelectedProfileId { get; set; }
|
||||
private string LoadMessage { get; set; } = "";
|
||||
private string LoadMessageType { get; set; } = "info";
|
||||
|
||||
private void OnProfileSelected(ChangeEventArgs e)
|
||||
{
|
||||
if (int.TryParse(e.Value?.ToString(), out int profileId))
|
||||
{
|
||||
SelectedProfileId = profileId;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedProfileId = 0;
|
||||
}
|
||||
LoadMessage = "";
|
||||
}
|
||||
|
||||
private async Task LoadSelectedProfile()
|
||||
{
|
||||
if (SelectedProfileId == 0 || Profiles == null)
|
||||
return;
|
||||
|
||||
var selectedProfile = Profiles.FirstOrDefault(p => p.Id == SelectedProfileId);
|
||||
if (selectedProfile != null)
|
||||
{
|
||||
LoadMessage = $"Profilo '{selectedProfile.Name}' caricato con successo!";
|
||||
LoadMessageType = "success";
|
||||
await OnProfileLoaded.InvokeAsync(selectedProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadMessage = "Errore nel caricamento del profilo selezionato.";
|
||||
LoadMessageType = "danger";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenProfileManagement()
|
||||
{
|
||||
await OnManageProfiles.InvokeAsync();
|
||||
}
|
||||
|
||||
public void ClearMessage()
|
||||
{
|
||||
LoadMessage = "";
|
||||
}
|
||||
|
||||
public void SetMessage(string message, string type = "info")
|
||||
{
|
||||
LoadMessage = message;
|
||||
LoadMessageType = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using CredentialManager.Models
|
||||
@using CredentialManager.Services
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 378 B |
Reference in New Issue
Block a user