Database Setup, raporlama, Demo, BackgroundWorker,
This commit is contained in:
parent
9689ef1bc6
commit
3446691162
14 changed files with 327 additions and 45 deletions
|
|
@ -10,36 +10,41 @@ namespace Sozsoft.Languages.Localization;
|
||||||
public class DatabaseLocalizationResourceContributor : ILocalizationResourceContributor
|
public class DatabaseLocalizationResourceContributor : ILocalizationResourceContributor
|
||||||
{
|
{
|
||||||
private LocalizationResourceBase _resource;
|
private LocalizationResourceBase _resource;
|
||||||
private IDatabaseResourceLocalizer _databaseResourceLocalizer;
|
private System.Lazy<IDatabaseResourceLocalizer> _databaseResourceLocalizer;
|
||||||
private ILanguageProvider _languageProvider;
|
private System.Lazy<ILanguageProvider> _languageProvider;
|
||||||
|
|
||||||
public bool IsDynamic => false;
|
public bool IsDynamic => false;
|
||||||
|
|
||||||
public void Initialize(LocalizationResourceInitializationContext context)
|
public void Initialize(LocalizationResourceInitializationContext context)
|
||||||
{
|
{
|
||||||
_resource = context.Resource;
|
_resource = context.Resource;
|
||||||
_databaseResourceLocalizer = context.ServiceProvider.GetRequiredService<IDatabaseResourceLocalizer>();
|
// ABP calls Initialize while AbpStringLocalizerFactory holds its internal lock.
|
||||||
_languageProvider = context.ServiceProvider.GetRequiredService<ILanguageProvider>();
|
// Resolving repositories/caches here can request another IStringLocalizer and
|
||||||
|
// re-enter the same lock, preventing Kestrel from ever starting.
|
||||||
|
_databaseResourceLocalizer = new System.Lazy<IDatabaseResourceLocalizer>(
|
||||||
|
() => context.ServiceProvider.GetRequiredService<IDatabaseResourceLocalizer>());
|
||||||
|
_languageProvider = new System.Lazy<ILanguageProvider>(
|
||||||
|
() => context.ServiceProvider.GetRequiredService<ILanguageProvider>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalizedString GetOrNull(string cultureName, string name)
|
public LocalizedString GetOrNull(string cultureName, string name)
|
||||||
{
|
{
|
||||||
return _databaseResourceLocalizer.GetOrNull(_resource, cultureName, name);
|
return _databaseResourceLocalizer.Value.GetOrNull(_resource, cultureName, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FillAsync(string cultureName, Dictionary<string, LocalizedString> dictionary)
|
public async Task FillAsync(string cultureName, Dictionary<string, LocalizedString> dictionary)
|
||||||
{
|
{
|
||||||
await _databaseResourceLocalizer.FillAsync(_resource, cultureName, dictionary);
|
await _databaseResourceLocalizer.Value.FillAsync(_resource, cultureName, dictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary)
|
public void Fill(string cultureName, Dictionary<string, LocalizedString> dictionary)
|
||||||
{
|
{
|
||||||
_databaseResourceLocalizer.Fill(_resource, cultureName, dictionary);
|
_databaseResourceLocalizer.Value.Fill(_resource, cultureName, dictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<string>> GetSupportedCulturesAsync()
|
public async Task<IEnumerable<string>> GetSupportedCulturesAsync()
|
||||||
{
|
{
|
||||||
var langs = await _languageProvider.GetLanguagesAsync();
|
var langs = await _languageProvider.Value.GetLanguagesAsync();
|
||||||
return langs.Select(a => a.CultureName).ToList();
|
return langs.Select(a => a.CultureName).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Volo.Abp.BackgroundJobs;
|
||||||
|
using Volo.Abp.BackgroundWorkers;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.OpenIddict.Tokens;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform;
|
||||||
|
|
||||||
|
public class AbpBackgroundWorkerRecurringJobInitializer : ITransientDependency
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IBackgroundWorkerManager _backgroundWorkerManager;
|
||||||
|
private readonly ILogger<AbpBackgroundWorkerRecurringJobInitializer> _logger;
|
||||||
|
|
||||||
|
public AbpBackgroundWorkerRecurringJobInitializer(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IBackgroundWorkerManager backgroundWorkerManager,
|
||||||
|
ILogger<AbpBackgroundWorkerRecurringJobInitializer> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_backgroundWorkerManager = backgroundWorkerManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await AddWorkerIfAvailableAsync<BackgroundJobWorker>(
|
||||||
|
nameof(BackgroundJobWorker),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await AddWorkerIfAvailableAsync<TokenCleanupBackgroundWorker>(
|
||||||
|
nameof(TokenCleanupBackgroundWorker),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddWorkerIfAvailableAsync<TWorker>(
|
||||||
|
string workerName,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
where TWorker : class, IBackgroundWorker
|
||||||
|
{
|
||||||
|
var worker = _serviceProvider.GetService<TWorker>();
|
||||||
|
if (worker == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"ABP background worker {WorkerName} could not be resolved from DI. Recurring job was not registered.",
|
||||||
|
workerName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _backgroundWorkerManager.AddAsync(worker, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"ABP background worker {WorkerName} recurring job registration has been ensured.",
|
||||||
|
workerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,20 +39,42 @@ public class BackgroundWorkerStartupService : IHostedService
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeBackgroundWorkersAsync()
|
private async Task InitializeBackgroundWorkersAsync()
|
||||||
|
{
|
||||||
|
const int maxAttempts = 5;
|
||||||
|
|
||||||
|
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Background worker startup initialization started.");
|
_logger.LogInformation(
|
||||||
|
"Background worker startup initialization started. Attempt {Attempt}/{MaxAttempts}.",
|
||||||
|
attempt,
|
||||||
|
maxAttempts);
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var abpRecurringJobInitializer = scope.ServiceProvider.GetRequiredService<AbpBackgroundWorkerRecurringJobInitializer>();
|
||||||
|
await abpRecurringJobInitializer.RunAsync();
|
||||||
|
|
||||||
var initializer = scope.ServiceProvider.GetRequiredService<BackgroundWorkerInitializer>();
|
var initializer = scope.ServiceProvider.GetRequiredService<BackgroundWorkerInitializer>();
|
||||||
await initializer.RunAsync();
|
await initializer.RunAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Background worker startup initialization completed.");
|
_logger.LogInformation("Background worker startup initialization completed.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (attempt == maxAttempts)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Background worker initialization failed.");
|
_logger.LogError(ex, "Background worker initialization failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Background worker initialization failed. It will be retried.");
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(3));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,19 @@ public class SetupController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IHostEnvironment _env;
|
private readonly IHostEnvironment _env;
|
||||||
|
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
|
||||||
|
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
|
||||||
|
|
||||||
public SetupController(
|
public SetupController(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IHostEnvironment env)
|
IHostEnvironment env,
|
||||||
|
BackgroundWorkerInitializer backgroundWorkerInitializer,
|
||||||
|
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_env = env;
|
_env = env;
|
||||||
|
_backgroundWorkerInitializer = backgroundWorkerInitializer;
|
||||||
|
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("application-status")]
|
[HttpGet("application-status")]
|
||||||
|
|
@ -38,8 +44,24 @@ public class SetupController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("migrate")]
|
[HttpPost("migrate")]
|
||||||
|
[AllowAnonymous]
|
||||||
public async Task Migrate(CancellationToken ct)
|
public async Task Migrate(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var databaseIsReady = SetupAppRunner.DatabaseIsReady(_configuration);
|
||||||
|
if (databaseIsReady)
|
||||||
|
{
|
||||||
|
if (User?.IsInRole("admin") != true)
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!SetupAppRunner.IsValidSetupPassword(_configuration, Request.Headers))
|
||||||
|
{
|
||||||
|
Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Response.ContentType = "text/event-stream; charset=utf-8";
|
Response.ContentType = "text/event-stream; charset=utf-8";
|
||||||
Response.Headers["Cache-Control"] = "no-cache, no-store";
|
Response.Headers["Cache-Control"] = "no-cache, no-store";
|
||||||
Response.Headers["X-Accel-Buffering"] = "no";
|
Response.Headers["X-Accel-Buffering"] = "no";
|
||||||
|
|
@ -141,7 +163,22 @@ public class SetupController : ControllerBase
|
||||||
|
|
||||||
if (process.ExitCode == 0)
|
if (process.ExitCode == 0)
|
||||||
{
|
{
|
||||||
await Send("success", "Migration and seed completed successfully.");
|
await Send("info", "Migration and seed completed successfully.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Send("info", "Loading background workers into Hangfire...");
|
||||||
|
await _abpBackgroundWorkerRecurringJobInitializer.RunAsync(ct);
|
||||||
|
await _backgroundWorkerInitializer.RunAsync();
|
||||||
|
await Send("success", "Background workers loaded successfully.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Send("error", $"Background workers could not be loaded: {ex.Message}");
|
||||||
|
await Send("done", "Failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Send("done", "Completed.");
|
await Send("done", "Completed.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ namespace Sozsoft.Platform;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class SetupAppRunner
|
internal static class SetupAppRunner
|
||||||
{
|
{
|
||||||
|
private const string SetupPasswordHeaderName = "X-Setup-Password";
|
||||||
|
|
||||||
// Veritabanı Hazırlık Kontrolü
|
// Veritabanı Hazırlık Kontrolü
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -51,6 +53,18 @@ internal static class SetupAppRunner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidSetupPassword(IConfiguration configuration, IHeaderDictionary headers)
|
||||||
|
{
|
||||||
|
var configuredPassword = configuration["Setup:MigrationPassword"];
|
||||||
|
if (string.IsNullOrWhiteSpace(configuredPassword))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers.TryGetValue(SetupPasswordHeaderName, out var providedPassword) &&
|
||||||
|
string.Equals(providedPassword.ToString(), configuredPassword, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool SqlServerIsReady(string connectionString)
|
private static bool SqlServerIsReady(string connectionString)
|
||||||
{
|
{
|
||||||
var csb = new SqlConnectionStringBuilder(connectionString);
|
var csb = new SqlConnectionStringBuilder(connectionString);
|
||||||
|
|
@ -175,6 +189,13 @@ internal static class SetupAppRunner
|
||||||
app.MapPost("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env,
|
app.MapPost("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env,
|
||||||
IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) =>
|
IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
|
if (!IsValidSetupPassword(cfg, ctx.Request.Headers))
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await ctx.Response.WriteAsync("Invalid setup password.", ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Response.ContentType = "text/event-stream; charset=utf-8";
|
ctx.Response.ContentType = "text/event-stream; charset=utf-8";
|
||||||
ctx.Response.Headers["Cache-Control"] = "no-cache, no-store";
|
ctx.Response.Headers["Cache-Control"] = "no-cache, no-store";
|
||||||
ctx.Response.Headers["X-Accel-Buffering"] = "no";
|
ctx.Response.Headers["X-Accel-Buffering"] = "no";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Volo.Abp;
|
using Volo.Abp;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
@ -29,6 +30,12 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||||
{
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments("/api/setup"))
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.User.Identity?.IsAuthenticated != true)
|
if (context.User.Identity?.IsAuthenticated != true)
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
|
|
@ -46,6 +53,8 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
||||||
|
|
||||||
var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value);
|
var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value);
|
||||||
using (currentTenant.Change(tenantId))
|
using (currentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||||
var sessions = await sessionRepository.GetListAsync(userId: userId);
|
var sessions = await sessionRepository.GetListAsync(userId: userId);
|
||||||
|
|
@ -58,6 +67,12 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (IsDatabaseUnavailableException(ex))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await next(context);
|
await next(context);
|
||||||
}
|
}
|
||||||
|
|
@ -66,4 +81,13 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
return !string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out var id) ? id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsDatabaseUnavailableException(Exception exception)
|
||||||
|
{
|
||||||
|
return exception is SqlException ||
|
||||||
|
exception is TimeoutException ||
|
||||||
|
exception is TaskCanceledException ||
|
||||||
|
exception is InvalidOperationException && exception.InnerException != null &&
|
||||||
|
IsDatabaseUnavailableException(exception.InnerException);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,11 @@ public class PlatformHttpApiHostModule : AbpModule
|
||||||
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
|
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
|
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
|
||||||
if (connectionString.IsNullOrWhiteSpace() || !SetupAppRunner.DatabaseIsReady(configuration))
|
// Program has already selected the full application only after the database
|
||||||
|
// readiness check succeeds. Repeating the probe here creates a race: one
|
||||||
|
// transient failure skips AddHangfire while ABP still starts its Hangfire
|
||||||
|
// modules, causing JobStorage initialization to crash the host.
|
||||||
|
if (connectionString.IsNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ public class DynamicGridReport : XtraReport
|
||||||
localizer);
|
localizer);
|
||||||
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
|
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
|
||||||
|
|
||||||
ConfigurePage(reportColumns);
|
ConfigurePage(reportColumns, isSubReport);
|
||||||
ConfigureDataSource(
|
ConfigureDataSource(
|
||||||
selectQuery,
|
selectQuery,
|
||||||
connectionString,
|
connectionString,
|
||||||
|
|
@ -162,13 +162,14 @@ public class DynamicGridReport : XtraReport
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1416
|
#pragma warning disable CA1416
|
||||||
private void ConfigurePage(IReadOnlyCollection<ReportColumn> visibleFields)
|
private void ConfigurePage(IReadOnlyCollection<ReportColumn> visibleFields, bool isSubReport)
|
||||||
{
|
{
|
||||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
||||||
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
||||||
var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right;
|
var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right;
|
||||||
var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field));
|
var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field));
|
||||||
Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth;
|
Landscape = !isSubReport &&
|
||||||
|
(visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth);
|
||||||
Font = new DXFont("Arial", 8.25F);
|
Font = new DXFont("Arial", 8.25F);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@
|
||||||
"RefreshTokenMinutes": 90
|
"RefreshTokenMinutes": 90
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Setup": {
|
||||||
|
"MigrationPassword": "sozsoft"
|
||||||
|
},
|
||||||
"StringEncryption": {
|
"StringEncryption": {
|
||||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,9 @@ export const useDemo = () => {
|
||||||
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useOptionalDemo = () => {
|
||||||
|
const context = useContext(DemoContext)
|
||||||
|
|
||||||
|
return context ?? { openDemo: () => undefined }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,10 @@ const Grid = (props: GridProps) => {
|
||||||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentUser = useStoreState((state) => state.auth.user)
|
const currentUser = useStoreState((state) => state.auth.user)
|
||||||
|
const currentCulture = useStoreState(
|
||||||
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||||
|
)
|
||||||
|
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
|
||||||
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
||||||
|
|
||||||
const gridRef = useRef<DataGridRef>()
|
const gridRef = useRef<DataGridRef>()
|
||||||
|
|
@ -1096,7 +1100,7 @@ const Grid = (props: GridProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return cols
|
return cols
|
||||||
}, [gridDto])
|
}, [gridDto, currentCulture, localizationTexts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumnData(memoizedColumns)
|
setColumnData(memoizedColumns)
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,10 @@ const Tree = (props: TreeProps) => {
|
||||||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentUser = useStoreState((state) => state.auth.user)
|
const currentUser = useStoreState((state) => state.auth.user)
|
||||||
|
const currentCulture = useStoreState(
|
||||||
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||||
|
)
|
||||||
|
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
|
||||||
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
||||||
|
|
||||||
const gridRef = useRef<TreeListRef>()
|
const gridRef = useRef<TreeListRef>()
|
||||||
|
|
@ -971,7 +975,7 @@ const Tree = (props: TreeProps) => {
|
||||||
cols,
|
cols,
|
||||||
)
|
)
|
||||||
setTreeListDataSource(dataSource)
|
setTreeListDataSource(dataSource)
|
||||||
}, [gridDto, searchParams])
|
}, [gridDto, searchParams, currentCulture, localizationTexts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeFilters = extraFilters.filter((f) => f.value)
|
const activeFilters = extraFilters.filter((f) => f.value)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { DesignerSelection } from './designer/types'
|
||||||
import { useDesignerState } from './designer/useDesignerState'
|
import { useDesignerState } from './designer/useDesignerState'
|
||||||
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
||||||
import { Button, Notification, toast } from '@/components/ui'
|
import { Button, Notification, toast } from '@/components/ui'
|
||||||
import { useDemo } from '@/contexts/DemoContext'
|
import { useOptionalDemo } from '@/contexts/DemoContext'
|
||||||
|
|
||||||
interface HomeSlideServiceContent {
|
interface HomeSlideServiceContent {
|
||||||
icon: string
|
icon: string
|
||||||
|
|
@ -273,7 +273,7 @@ function buildHomeContent(home: HomeDto | undefined, translate: (key: string) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const { openDemo } = useDemo()
|
const { openDemo } = useOptionalDemo()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { setLang } = useStoreActions((actions) => actions.locale)
|
const { setLang } = useStoreActions((actions) => actions.locale)
|
||||||
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
|
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { getMigrateUrl, getSetupStatus } from '@/services/setup.service'
|
import { getMigrateUrl, getSetupStatus } from '@/services/setup.service'
|
||||||
import { applicationConfigurationUrl } from '@/services/abpConfig.service'
|
import { applicationConfigurationUrl } from '@/services/abpConfig.service'
|
||||||
import { Button } from '@/components/ui'
|
import { Button, Dialog } from '@/components/ui'
|
||||||
|
|
||||||
interface LogLine {
|
interface LogLine {
|
||||||
level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done'
|
level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done'
|
||||||
|
|
@ -79,6 +79,8 @@ const DatabaseSetup = () => {
|
||||||
const [status, setStatus] = useState<MigrationStatus>('idle')
|
const [status, setStatus] = useState<MigrationStatus>('idle')
|
||||||
const [pollCountdown, setPollCountdown] = useState(0)
|
const [pollCountdown, setPollCountdown] = useState(0)
|
||||||
const [dbExists, setDbExists] = useState<boolean | null>(null)
|
const [dbExists, setDbExists] = useState<boolean | null>(null)
|
||||||
|
const [setupPassword, setSetupPassword] = useState('')
|
||||||
|
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false)
|
||||||
const logEndRef = useRef<HTMLDivElement>(null)
|
const logEndRef = useRef<HTMLDivElement>(null)
|
||||||
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
|
@ -119,9 +121,15 @@ const DatabaseSetup = () => {
|
||||||
|
|
||||||
const startMigration = async () => {
|
const startMigration = async () => {
|
||||||
if (status === 'running') return
|
if (status === 'running') return
|
||||||
|
if (!setupPassword.trim()) {
|
||||||
|
setStatus('error')
|
||||||
|
addLog('error', 'Setup password is required.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setLogs([])
|
setLogs([])
|
||||||
setStatus('running')
|
setStatus('running')
|
||||||
|
setIsPasswordDialogOpen(false)
|
||||||
addLog('info', 'Starting migration...')
|
addLog('info', 'Starting migration...')
|
||||||
|
|
||||||
const url = getMigrateUrl()
|
const url = getMigrateUrl()
|
||||||
|
|
@ -152,6 +160,7 @@ const DatabaseSetup = () => {
|
||||||
|
|
||||||
if (level === 'success') {
|
if (level === 'success') {
|
||||||
setStatus('success')
|
setStatus('success')
|
||||||
|
pollUntilReady()
|
||||||
} else if (level === 'error') {
|
} else if (level === 'error') {
|
||||||
setStatus('error')
|
setStatus('error')
|
||||||
} else if (level === 'restart') {
|
} else if (level === 'restart') {
|
||||||
|
|
@ -166,12 +175,20 @@ const DatabaseSetup = () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Accept: 'text/event-stream' },
|
headers: {
|
||||||
|
Accept: 'text/event-stream',
|
||||||
|
'X-Setup-Password': setupPassword,
|
||||||
|
},
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
addLog('error', `Server responded with status ${response.status}`)
|
addLog(
|
||||||
|
'error',
|
||||||
|
response.status === 403
|
||||||
|
? 'Invalid setup password or setup migration is not allowed.'
|
||||||
|
: `Server responded with status ${response.status}`,
|
||||||
|
)
|
||||||
setStatus('error')
|
setStatus('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -196,9 +213,29 @@ const DatabaseSetup = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-6 text-white">
|
<div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-6 text-white">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-10 text-center sm:text-left">
|
||||||
<h1 className="text-3xl font-bold mb-2">{APP_NAME}</h1>
|
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||||
<p className="text-gray-400 text-lg">Initial Setup — Creating Database</p>
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 rounded-full bg-sky-400/30 blur-2xl" />
|
||||||
|
<div className="relative flex h-20 w-20 items-center justify-center">
|
||||||
|
<img
|
||||||
|
src="/img/logo/logo-dark-streamline.png"
|
||||||
|
alt={`${APP_NAME} logo`}
|
||||||
|
className="h-18 w-18 object-contain drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="tracking-tight text-white">
|
||||||
|
{APP_NAME}
|
||||||
|
</h1>
|
||||||
|
<div className="mt-1 inline-flex items-center gap-2 rounded-full border border-emerald-400/20 bg-emerald-400/10 px-1 py-1 text-xs font-medium uppercase tracking-[0.22em] text-emerald-200">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-emerald-300 shadow-[0_0_12px_rgba(110,231,183,0.85)]" />
|
||||||
|
Database creation and migration workspace
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Card */}
|
{/* Status Card */}
|
||||||
|
|
@ -234,7 +271,7 @@ const DatabaseSetup = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Area */}
|
{/* Action Area */}
|
||||||
<div className="flex items-center justify-between px-5 py-4 bg-gray-700 border-t border-gray-600">
|
<div className="flex items-center justify-between gap-4 px-5 py-4 bg-gray-700 border-t border-gray-600">
|
||||||
<div className="text-xs text-gray-400">
|
<div className="text-xs text-gray-400">
|
||||||
{status === 'idle' && dbExists === true && 'Database already exists. Migration is not required.'}
|
{status === 'idle' && dbExists === true && 'Database already exists. Migration is not required.'}
|
||||||
{status === 'idle' && dbExists === false && 'Database not found. Press the button to start migration.'}
|
{status === 'idle' && dbExists === false && 'Database not found. Press the button to start migration.'}
|
||||||
|
|
@ -251,9 +288,10 @@ const DatabaseSetup = () => {
|
||||||
{(status === 'idle' || status === 'error') && !dbExists && (
|
{(status === 'idle' || status === 'error') && !dbExists && (
|
||||||
<Button
|
<Button
|
||||||
variant='solid'
|
variant='solid'
|
||||||
onClick={startMigration}
|
className="min-w-40"
|
||||||
|
onClick={() => setIsPasswordDialogOpen(true)}
|
||||||
>
|
>
|
||||||
{status === 'error' ? 'Retry' : 'Start Setup'}
|
{status === 'error' ? 'Retry Migration' : 'Migrate'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -267,6 +305,59 @@ const DatabaseSetup = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
isOpen={isPasswordDialogOpen}
|
||||||
|
onClose={() => setIsPasswordDialogOpen(false)}
|
||||||
|
onRequestClose={() => setIsPasswordDialogOpen(false)}
|
||||||
|
width={440}
|
||||||
|
>
|
||||||
|
<Dialog.Header className="-mx-6 -mt-6 border-b border-gray-200 px-6 py-4 dark:border-gray-700">
|
||||||
|
<div>
|
||||||
|
<h5 className="text-gray-900 dark:text-gray-100">Setup authorization</h5>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Enter the setup password to start database migration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Dialog.Header>
|
||||||
|
<Dialog.Body className="pt-5">
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
Setup password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="password"
|
||||||
|
value={setupPassword}
|
||||||
|
onChange={(event) => setSetupPassword(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' && setupPassword.trim()) {
|
||||||
|
startMigration()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Enter setup password"
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
|
||||||
|
/>
|
||||||
|
<p className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
This protects the migration action when users are redirected to setup after a database
|
||||||
|
problem.
|
||||||
|
</p>
|
||||||
|
</Dialog.Body>
|
||||||
|
<Dialog.Footer className="mt-2 flex justify-end gap-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsPasswordDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
disabled={!setupPassword.trim() || status === 'running'}
|
||||||
|
onClick={startMigration}
|
||||||
|
>
|
||||||
|
Start Migration
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<p className="mt-6 text-xs text-gray-600">
|
<p className="mt-6 text-xs text-gray-600">
|
||||||
This page is only visible when the database does not exist.
|
This page is only visible when the database does not exist.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue