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 |
@@ -40,6 +40,7 @@ public static class CredentialManagerConfiguration
|
||||
services.AddScoped<IEncryptionService, EncryptionService>();
|
||||
services.AddScoped<ICredentialService, CredentialService>();
|
||||
services.AddScoped<IDatabaseInitializer, DatabaseInitializer>();
|
||||
services.AddScoped<IDataCouplerProfileService, DataCouplerProfileService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public class CredentialDbContext : DbContext
|
||||
{
|
||||
public DbSet<CredentialEntity> Credentials { get; set; }
|
||||
public DbSet<KeyAssociation> KeyAssociations { get; set; }
|
||||
public DbSet<DataCouplerProfile> DataCouplerProfiles { get; set; }
|
||||
|
||||
public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options)
|
||||
{
|
||||
@@ -141,5 +142,80 @@ public class CredentialDbContext : DbContext
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
entity.HasIndex(e => e.LastVerifiedAt);
|
||||
});
|
||||
|
||||
// Configurazione della tabella DataCouplerProfiles
|
||||
modelBuilder.Entity<DataCouplerProfile>(entity =>
|
||||
{
|
||||
entity.ToTable("DataCouplerProfiles");
|
||||
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
entity.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100);
|
||||
|
||||
entity.Property(e => e.Description)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.Property(e => e.SourceType)
|
||||
.IsRequired()
|
||||
.HasMaxLength(20);
|
||||
|
||||
entity.Property(e => e.SourceSchema)
|
||||
.HasMaxLength(200);
|
||||
|
||||
entity.Property(e => e.SourceTable)
|
||||
.HasMaxLength(200);
|
||||
|
||||
entity.Property(e => e.SourceFilePath)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.Property(e => e.DestinationType)
|
||||
.IsRequired()
|
||||
.HasMaxLength(20);
|
||||
|
||||
entity.Property(e => e.DestinationSchema)
|
||||
.HasMaxLength(200);
|
||||
|
||||
entity.Property(e => e.DestinationTable)
|
||||
.HasMaxLength(200);
|
||||
|
||||
entity.Property(e => e.DestinationEndpoint)
|
||||
.HasMaxLength(500);
|
||||
|
||||
entity.Property(e => e.FieldMappingJson)
|
||||
.HasMaxLength(4000);
|
||||
|
||||
entity.Property(e => e.CreatedBy)
|
||||
.HasMaxLength(100);
|
||||
|
||||
// Valori di default
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
entity.Property(e => e.IsActive)
|
||||
.HasDefaultValue(true);
|
||||
|
||||
// Indici
|
||||
entity.HasIndex(e => e.Name)
|
||||
.IsUnique();
|
||||
|
||||
entity.HasIndex(e => e.SourceType);
|
||||
entity.HasIndex(e => e.DestinationType);
|
||||
entity.HasIndex(e => e.IsActive);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
entity.HasIndex(e => e.LastUsedAt);
|
||||
|
||||
// Relazioni con le credenziali
|
||||
entity.HasOne(e => e.SourceCredential)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.SourceCredentialId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(e => e.DestinationCredential)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.DestinationCredentialId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace CredentialManager.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Factory per creare il DbContext durante la fase di design (migrations)
|
||||
/// </summary>
|
||||
public class CredentialDbContextFactory : IDesignTimeDbContextFactory<CredentialDbContext>
|
||||
{
|
||||
public CredentialDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<CredentialDbContext>();
|
||||
|
||||
// Usa un database SQLite temporaneo per le migrations
|
||||
var connectionString = "Data Source=design_time_temp.db";
|
||||
optionsBuilder.UseSqlite(connectionString);
|
||||
|
||||
return new CredentialDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CredentialManager.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CredentialManager.Migrations
|
||||
{
|
||||
[DbContext(typeof(CredentialDbContext))]
|
||||
[Migration("20250701203438_AddDataCouplerProfiles")]
|
||||
partial class AddDataCouplerProfiles
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CommandTimeout")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30);
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedApiKey")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedAuthToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedPassword")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Headers")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IgnoreSslErrors")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RestServiceType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TimeoutSeconds")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(100);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DatabaseType");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Type");
|
||||
|
||||
b.ToTable("Credentials", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DestinationCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DestinationEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SourceCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DestinationCredentialId");
|
||||
|
||||
b.HasIndex("DestinationType");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LastUsedAt");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SourceCredentialId");
|
||||
|
||||
b.HasIndex("SourceType");
|
||||
|
||||
b.ToTable("DataCouplerProfiles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationEntity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("KeyValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastVerifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RestCredentialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourcesInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DestinationEntity");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("KeyValue")
|
||||
.HasDatabaseName("IX_KeyAssociations_KeyValue");
|
||||
|
||||
b.HasIndex("LastVerifiedAt");
|
||||
|
||||
b.HasIndex("RestCredentialName");
|
||||
|
||||
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_KeyAssociations_Unique");
|
||||
|
||||
b.ToTable("KeyAssociations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||
{
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("DestinationCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("SourceCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DestinationCredential");
|
||||
|
||||
b.Navigation("SourceCredential");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CredentialManager.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDataCouplerProfiles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DataCouplerProfiles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
SourceType = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
SourceCredentialId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
SourceSchema = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SourceTable = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SourceFilePath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
DestinationType = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
DestinationCredentialId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
DestinationSchema = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
DestinationTable = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
DestinationEndpoint = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
FieldMappingJson = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
LastUsedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DataCouplerProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DataCouplerProfiles_Credentials_DestinationCredentialId",
|
||||
column: x => x.DestinationCredentialId,
|
||||
principalTable: "Credentials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_DataCouplerProfiles_Credentials_SourceCredentialId",
|
||||
column: x => x.SourceCredentialId,
|
||||
principalTable: "Credentials",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_CreatedAt",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_DestinationCredentialId",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "DestinationCredentialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_DestinationType",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "DestinationType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_IsActive",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_LastUsedAt",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "LastUsedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_Name",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_SourceCredentialId",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "SourceCredentialId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataCouplerProfiles_SourceType",
|
||||
table: "DataCouplerProfiles",
|
||||
column: "SourceType");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DataCouplerProfiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace CredentialManager.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||
{
|
||||
@@ -123,6 +123,104 @@ namespace CredentialManager.Migrations
|
||||
b.ToTable("Credentials", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DestinationCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DestinationEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SourceCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DestinationCredentialId");
|
||||
|
||||
b.HasIndex("DestinationType");
|
||||
|
||||
b.HasIndex("IsActive");
|
||||
|
||||
b.HasIndex("LastUsedAt");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SourceCredentialId");
|
||||
|
||||
b.HasIndex("SourceType");
|
||||
|
||||
b.ToTable("DataCouplerProfiles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -202,6 +300,23 @@ namespace CredentialManager.Migrations
|
||||
|
||||
b.ToTable("KeyAssociations", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||
{
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("DestinationCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
|
||||
.WithMany()
|
||||
.HasForeignKey("SourceCredentialId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DestinationCredential");
|
||||
|
||||
b.Navigation("SourceCredential");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace CredentialManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Modello per salvare le configurazioni dei profili di Data Coupler
|
||||
/// </summary>
|
||||
public class DataCouplerProfile
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Configurazione Fonte Dati
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string SourceType { get; set; } = string.Empty; // "database" o "file"
|
||||
|
||||
public int? SourceCredentialId { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? SourceSchema { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? SourceTable { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? SourceFilePath { get; set; }
|
||||
|
||||
// Configurazione Destinazione
|
||||
[Required]
|
||||
[MaxLength(20)]
|
||||
public string DestinationType { get; set; } = string.Empty; // "database" o "rest"
|
||||
|
||||
public int? DestinationCredentialId { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? DestinationSchema { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? DestinationTable { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? DestinationEndpoint { get; set; }
|
||||
|
||||
// Mapping dei campi salvato come JSON
|
||||
[MaxLength(4000)]
|
||||
public string? FieldMappingJson { get; set; }
|
||||
|
||||
// Metadati
|
||||
[MaxLength(100)]
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? LastUsedAt { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Relazioni opzionali con le credenziali
|
||||
[ForeignKey(nameof(SourceCredentialId))]
|
||||
public virtual CredentialEntity? SourceCredential { get; set; }
|
||||
|
||||
[ForeignKey(nameof(DestinationCredentialId))]
|
||||
public virtual CredentialEntity? DestinationCredential { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace CredentialManager.Models;
|
||||
|
||||
/// <summary>
|
||||
/// DTO per la creazione/aggiornamento di un profilo DataCoupler
|
||||
/// </summary>
|
||||
public class DataCouplerProfileDto
|
||||
{
|
||||
public int? Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Informazioni sorgente
|
||||
public string SourceType { get; set; } = string.Empty;
|
||||
public int? SourceCredentialId { get; set; }
|
||||
public string? SourceSchema { get; set; }
|
||||
public string? SourceTable { get; set; }
|
||||
public string? SourceFilePath { get; set; }
|
||||
|
||||
// Informazioni destinazione
|
||||
public string DestinationType { get; set; } = string.Empty;
|
||||
public int? DestinationCredentialId { get; set; }
|
||||
public string? DestinationSchema { get; set; }
|
||||
public string? DestinationTable { get; set; }
|
||||
public string? DestinationEndpoint { get; set; }
|
||||
|
||||
// Mapping dei campi
|
||||
public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO per il mapping dei campi
|
||||
/// </summary>
|
||||
public class FieldMappingDto
|
||||
{
|
||||
public string SourceField { get; set; } = string.Empty;
|
||||
public string DestinationField { get; set; } = string.Empty;
|
||||
public string? DataType { get; set; }
|
||||
public bool IsKey { get; set; }
|
||||
public bool IsRequired { get; set; }
|
||||
public string? DefaultValue { get; set; }
|
||||
public string? Transformation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO per la visualizzazione di un profilo nella lista
|
||||
/// </summary>
|
||||
public class DataCouplerProfileSummaryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public string SourceType { get; set; } = string.Empty;
|
||||
public string? SourceName { get; set; }
|
||||
public string DestinationType { get; set; } = string.Empty;
|
||||
public string? DestinationName { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? LastUsedAt { get; set; }
|
||||
public string? CreatedBy { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CredentialManager.Data;
|
||||
using CredentialManager.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CredentialManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementazione del servizio per la gestione dei profili Data Coupler
|
||||
/// </summary>
|
||||
public class DataCouplerProfileService : IDataCouplerProfileService
|
||||
{
|
||||
private readonly CredentialDbContext _context;
|
||||
|
||||
public DataCouplerProfileService(CredentialDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutti i profili attivi
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<DataCouplerProfile>> GetAllProfilesAsync()
|
||||
{
|
||||
return await _context.DataCouplerProfiles
|
||||
.Include(p => p.SourceCredential)
|
||||
.Include(p => p.DestinationCredential)
|
||||
.Where(p => p.IsActive)
|
||||
.OrderByDescending(p => p.LastUsedAt)
|
||||
.ThenByDescending(p => p.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un profilo per ID
|
||||
/// </summary>
|
||||
public async Task<DataCouplerProfile?> GetProfileByIdAsync(int id)
|
||||
{
|
||||
return await _context.DataCouplerProfiles
|
||||
.Include(p => p.SourceCredential)
|
||||
.Include(p => p.DestinationCredential)
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.IsActive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un profilo per nome
|
||||
/// </summary>
|
||||
public async Task<DataCouplerProfile?> GetProfileByNameAsync(string name)
|
||||
{
|
||||
return await _context.DataCouplerProfiles
|
||||
.Include(p => p.SourceCredential)
|
||||
.Include(p => p.DestinationCredential)
|
||||
.FirstOrDefaultAsync(p => p.Name == name && p.IsActive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Salva un nuovo profilo
|
||||
/// </summary>
|
||||
public async Task<DataCouplerProfile> SaveProfileAsync(DataCouplerProfile profile)
|
||||
{
|
||||
profile.CreatedAt = DateTime.UtcNow;
|
||||
profile.IsActive = true;
|
||||
|
||||
_context.DataCouplerProfiles.Add(profile);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna un profilo esistente
|
||||
/// </summary>
|
||||
public async Task<DataCouplerProfile> UpdateProfileAsync(DataCouplerProfile profile)
|
||||
{
|
||||
var existingProfile = await _context.DataCouplerProfiles
|
||||
.FirstOrDefaultAsync(p => p.Id == profile.Id);
|
||||
|
||||
if (existingProfile == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Profilo con ID {profile.Id} non trovato");
|
||||
}
|
||||
|
||||
// Aggiorna le proprietà
|
||||
existingProfile.Name = profile.Name;
|
||||
existingProfile.Description = profile.Description;
|
||||
existingProfile.SourceType = profile.SourceType;
|
||||
existingProfile.SourceCredentialId = profile.SourceCredentialId;
|
||||
existingProfile.SourceSchema = profile.SourceSchema;
|
||||
existingProfile.SourceTable = profile.SourceTable;
|
||||
existingProfile.SourceFilePath = profile.SourceFilePath;
|
||||
existingProfile.DestinationType = profile.DestinationType;
|
||||
existingProfile.DestinationCredentialId = profile.DestinationCredentialId;
|
||||
existingProfile.DestinationSchema = profile.DestinationSchema;
|
||||
existingProfile.DestinationTable = profile.DestinationTable;
|
||||
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
||||
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return existingProfile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Elimina un profilo (soft delete)
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteProfileAsync(int id)
|
||||
{
|
||||
var profile = await _context.DataCouplerProfiles
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
profile.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna la data di ultimo utilizzo di un profilo
|
||||
/// </summary>
|
||||
public async Task UpdateLastUsedAsync(int id)
|
||||
{
|
||||
var profile = await _context.DataCouplerProfiles
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
profile.LastUsedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se esiste un profilo con il nome specificato
|
||||
/// </summary>
|
||||
public async Task<bool> ProfileExistsAsync(string name, int? excludeId = null)
|
||||
{
|
||||
var query = _context.DataCouplerProfiles
|
||||
.Where(p => p.Name == name && p.IsActive);
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializza la lista di mapping dei campi in JSON
|
||||
/// </summary>
|
||||
public string SerializeFieldMappings(List<FieldMappingDto>? mappings)
|
||||
{
|
||||
if (mappings == null || !mappings.Any())
|
||||
return string.Empty;
|
||||
|
||||
return JsonSerializer.Serialize(mappings, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializza il JSON dei mapping dei campi
|
||||
/// </summary>
|
||||
public List<FieldMappingDto> DeserializeFieldMappings(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return new List<FieldMappingDto>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<FieldMappingDto>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
}) ?? new List<FieldMappingDto>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<FieldMappingDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converte un DataCouplerProfile in DTO
|
||||
/// </summary>
|
||||
public DataCouplerProfileDto ToDto(DataCouplerProfile profile)
|
||||
{
|
||||
return new DataCouplerProfileDto
|
||||
{
|
||||
Id = profile.Id,
|
||||
Name = profile.Name,
|
||||
Description = profile.Description,
|
||||
SourceType = profile.SourceType,
|
||||
SourceCredentialId = profile.SourceCredentialId,
|
||||
SourceSchema = profile.SourceSchema,
|
||||
SourceTable = profile.SourceTable,
|
||||
SourceFilePath = profile.SourceFilePath,
|
||||
DestinationType = profile.DestinationType,
|
||||
DestinationCredentialId = profile.DestinationCredentialId,
|
||||
DestinationSchema = profile.DestinationSchema,
|
||||
DestinationTable = profile.DestinationTable,
|
||||
DestinationEndpoint = profile.DestinationEndpoint,
|
||||
FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converte un DTO in DataCouplerProfile
|
||||
/// </summary>
|
||||
public DataCouplerProfile FromDto(DataCouplerProfileDto dto, string? createdBy = null)
|
||||
{
|
||||
return new DataCouplerProfile
|
||||
{
|
||||
Id = dto.Id ?? 0,
|
||||
Name = dto.Name,
|
||||
Description = dto.Description,
|
||||
SourceType = dto.SourceType,
|
||||
SourceCredentialId = dto.SourceCredentialId,
|
||||
SourceSchema = dto.SourceSchema,
|
||||
SourceTable = dto.SourceTable,
|
||||
SourceFilePath = dto.SourceFilePath,
|
||||
DestinationType = dto.DestinationType,
|
||||
DestinationCredentialId = dto.DestinationCredentialId,
|
||||
DestinationSchema = dto.DestinationSchema,
|
||||
DestinationTable = dto.DestinationTable,
|
||||
DestinationEndpoint = dto.DestinationEndpoint,
|
||||
FieldMappingJson = SerializeFieldMappings(dto.FieldMappings),
|
||||
CreatedBy = createdBy
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using CredentialManager.Models;
|
||||
|
||||
namespace CredentialManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interfaccia per il servizio di gestione dei profili Data Coupler
|
||||
/// </summary>
|
||||
public interface IDataCouplerProfileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Ottiene tutti i profili attivi
|
||||
/// </summary>
|
||||
Task<IEnumerable<DataCouplerProfile>> GetAllProfilesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un profilo per ID
|
||||
/// </summary>
|
||||
Task<DataCouplerProfile?> GetProfileByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un profilo per nome
|
||||
/// </summary>
|
||||
Task<DataCouplerProfile?> GetProfileByNameAsync(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Salva un nuovo profilo
|
||||
/// </summary>
|
||||
Task<DataCouplerProfile> SaveProfileAsync(DataCouplerProfile profile);
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna un profilo esistente
|
||||
/// </summary>
|
||||
Task<DataCouplerProfile> UpdateProfileAsync(DataCouplerProfile profile);
|
||||
|
||||
/// <summary>
|
||||
/// Elimina un profilo
|
||||
/// </summary>
|
||||
Task<bool> DeleteProfileAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna la data di ultimo utilizzo di un profilo
|
||||
/// </summary>
|
||||
Task UpdateLastUsedAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se esiste un profilo con il nome specificato
|
||||
/// </summary>
|
||||
Task<bool> ProfileExistsAsync(string name, int? excludeId = null);
|
||||
}
|
||||
Binary file not shown.
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataConnection", "DataConne
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManager", "CredentialManager\CredentialManager.csproj", "{30B369DE-A0BA-4AD7-8895-7BEBD244E782}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{B5114CAC-3E03-4150-B93C-652882F66CB7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -55,6 +57,18 @@ Global
|
||||
{30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x64.Build.0 = Release|Any CPU
|
||||
{30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{30B369DE-A0BA-4AD7-8895-7BEBD244E782}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DataConnection\DataConnection.csproj" />
|
||||
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
|
||||
<ProjectReference Include="..\Components\Components.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@inject IDataConnectionFactory ConnectionFactory
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ILogger<DataCoupler> Logger
|
||||
@inject CredentialManager.Services.IDataCouplerProfileService ProfileService
|
||||
|
||||
<PageTitle>Data Coupler</PageTitle>
|
||||
|
||||
@@ -24,7 +25,19 @@
|
||||
<h3><i class="fas fa-exchange-alt"></i> Data Coupler - Coupling Database e REST API</h3>
|
||||
<p class="text-muted">Connetti database e servizi REST per il trasferimento dati</p>
|
||||
</div>
|
||||
</div> <div class="row">
|
||||
</div>
|
||||
|
||||
<!-- Sezione Gestione Profili -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ProfileSelector Profiles="availableProfiles"
|
||||
OnProfileLoaded="OnProfileLoaded"
|
||||
OnManageProfiles="OnManageProfiles"
|
||||
IsLoading="isLoadingProfiles" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Lato Sinistro - Fonte Dati -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
@@ -1103,6 +1116,31 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Sezione Salvataggio Profilo -->
|
||||
@if (isDatabaseConnected && isRestConnected && fieldMappings.Any())
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<ProfileSaver CanSave="CanSaveProfile()"
|
||||
SourceType="selectedSourceType"
|
||||
SourceSchema="@(databaseTables.Keys.FirstOrDefault()?.Split('.').FirstOrDefault())"
|
||||
SourceTable="selectedTable"
|
||||
DestinationType="rest"
|
||||
DestinationEndpoint="@(selectedRestEntity?.Name)"
|
||||
FieldMappings="GetCurrentFieldMappings()"
|
||||
OnProfileSaved="OnProfileSaved" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Componente Gestione Profili -->
|
||||
<ProfileManagement ShowModal="showProfileManagement"
|
||||
Profiles="availableProfiles"
|
||||
OnCloseModal="OnCloseProfileManagement"
|
||||
OnProfileLoaded="OnProfileLoaded"
|
||||
OnProfileDeleted="OnProfileDeleted"
|
||||
IsLoading="isLoadingProfiles" />
|
||||
|
||||
<!-- Modal per la selezione del database -->
|
||||
@if (showDatabaseSelectionModal)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@@ -8,3 +7,4 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using Data_Coupler
|
||||
@using Data_Coupler.Shared
|
||||
@using Components
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CredentialManager;
|
||||
using CredentialManager.Services;
|
||||
using CredentialManager.Models;
|
||||
|
||||
Console.WriteLine("🧪 Testing DataCouplerProfile Service...");
|
||||
|
||||
try
|
||||
{
|
||||
// Crea un service provider con CredentialManager
|
||||
var serviceProvider = await CredentialManagerFactory.CreateServiceProviderAsync();
|
||||
|
||||
// Ottieni il servizio per i profili
|
||||
var profileService = serviceProvider.GetRequiredService<IDataCouplerProfileService>();
|
||||
|
||||
Console.WriteLine("✅ Service created successfully!");
|
||||
|
||||
// Test: Ottieni tutti i profili (dovrebbe essere vuoto)
|
||||
var profiles = await profileService.GetAllProfilesAsync();
|
||||
Console.WriteLine($"📋 Found {profiles.Count()} existing profiles");
|
||||
|
||||
// Test: Crea un profilo di test
|
||||
var testProfile = new DataCouplerProfile
|
||||
{
|
||||
Name = "Test Profile",
|
||||
Description = "Profile creato durante il test",
|
||||
SourceType = "database",
|
||||
DestinationType = "rest",
|
||||
SourceSchema = "dbo",
|
||||
SourceTable = "customers",
|
||||
DestinationEndpoint = "/api/customers",
|
||||
CreatedBy = "System Test"
|
||||
};
|
||||
|
||||
// Salva il profilo
|
||||
var savedProfile = await profileService.SaveProfileAsync(testProfile);
|
||||
Console.WriteLine($"💾 Test profile saved with ID: {savedProfile.Id}");
|
||||
|
||||
// Ricarica i profili
|
||||
profiles = await profileService.GetAllProfilesAsync();
|
||||
Console.WriteLine($"📋 Now found {profiles.Count()} profiles");
|
||||
|
||||
// Elimina il profilo di test
|
||||
var deleted = await profileService.DeleteProfileAsync(savedProfile.Id);
|
||||
Console.WriteLine($"🗑️ Test profile deleted: {deleted}");
|
||||
|
||||
Console.WriteLine("✅ All tests passed! DataCouplerProfile service is working correctly.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Error during testing: {ex.Message}");
|
||||
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
|
||||
Console.WriteLine("Press any key to exit...");
|
||||
Console.ReadKey();
|
||||
+1
-6
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -10,10 +11,4 @@
|
||||
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,50 +0,0 @@
|
||||
using CredentialManager.Data;
|
||||
using CredentialManager.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TestDatabaseFix;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Test Database Initialization Fix");
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder => builder.AddConsole());
|
||||
|
||||
// Configura il DbContext per usare SQLite
|
||||
services.AddDbContext<CredentialDbContext>(options =>
|
||||
options.UseSqlite("Data Source=test_credentials.db"));
|
||||
|
||||
services.AddScoped<DatabaseInitializer>();
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<CredentialDbContext>();
|
||||
var initializer = scope.ServiceProvider.GetRequiredService<DatabaseInitializer>();
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Inizializzando il database...");
|
||||
await initializer.InitializeAsync();
|
||||
|
||||
Console.WriteLine("Verifica tabelle...");
|
||||
var credentialsCount = await dbContext.Credentials.CountAsync();
|
||||
var associationsCount = await dbContext.RecordAssociations.CountAsync();
|
||||
|
||||
Console.WriteLine($"Tabella Credentials: {credentialsCount} record");
|
||||
Console.WriteLine($"Tabella RecordAssociations: {associationsCount} record");
|
||||
|
||||
Console.WriteLine("Test completato con successo!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Errore: {ex.Message}");
|
||||
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user