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
|
||||
{
|
||||
private LocalizationResourceBase _resource;
|
||||
private IDatabaseResourceLocalizer _databaseResourceLocalizer;
|
||||
private ILanguageProvider _languageProvider;
|
||||
private System.Lazy<IDatabaseResourceLocalizer> _databaseResourceLocalizer;
|
||||
private System.Lazy<ILanguageProvider> _languageProvider;
|
||||
|
||||
public bool IsDynamic => false;
|
||||
|
||||
public void Initialize(LocalizationResourceInitializationContext context)
|
||||
{
|
||||
_resource = context.Resource;
|
||||
_databaseResourceLocalizer = context.ServiceProvider.GetRequiredService<IDatabaseResourceLocalizer>();
|
||||
_languageProvider = context.ServiceProvider.GetRequiredService<ILanguageProvider>();
|
||||
// ABP calls Initialize while AbpStringLocalizerFactory holds its internal lock.
|
||||
// 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)
|
||||
{
|
||||
return _databaseResourceLocalizer.GetOrNull(_resource, cultureName, name);
|
||||
return _databaseResourceLocalizer.Value.GetOrNull(_resource, cultureName, name);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_databaseResourceLocalizer.Fill(_resource, cultureName, dictionary);
|
||||
_databaseResourceLocalizer.Value.Fill(_resource, cultureName, dictionary);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetSupportedCulturesAsync()
|
||||
{
|
||||
var langs = await _languageProvider.GetLanguagesAsync();
|
||||
var langs = await _languageProvider.Value.GetLanguagesAsync();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,19 +40,41 @@ public class BackgroundWorkerStartupService : IHostedService
|
|||
|
||||
private async Task InitializeBackgroundWorkersAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Background worker startup initialization started.");
|
||||
const int maxAttempts = 5;
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var initializer = scope.ServiceProvider.GetRequiredService<BackgroundWorkerInitializer>();
|
||||
await initializer.RunAsync();
|
||||
|
||||
_logger.LogInformation("Background worker startup initialization completed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
_logger.LogError(ex, "Background worker initialization failed.");
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Background worker startup initialization started. Attempt {Attempt}/{MaxAttempts}.",
|
||||
attempt,
|
||||
maxAttempts);
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var abpRecurringJobInitializer = scope.ServiceProvider.GetRequiredService<AbpBackgroundWorkerRecurringJobInitializer>();
|
||||
await abpRecurringJobInitializer.RunAsync();
|
||||
|
||||
var initializer = scope.ServiceProvider.GetRequiredService<BackgroundWorkerInitializer>();
|
||||
await initializer.RunAsync();
|
||||
|
||||
_logger.LogInformation("Background worker startup initialization completed.");
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (attempt == maxAttempts)
|
||||
{
|
||||
_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 IHostEnvironment _env;
|
||||
private readonly BackgroundWorkerInitializer _backgroundWorkerInitializer;
|
||||
private readonly AbpBackgroundWorkerRecurringJobInitializer _abpBackgroundWorkerRecurringJobInitializer;
|
||||
|
||||
public SetupController(
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment env)
|
||||
IHostEnvironment env,
|
||||
BackgroundWorkerInitializer backgroundWorkerInitializer,
|
||||
AbpBackgroundWorkerRecurringJobInitializer abpBackgroundWorkerRecurringJobInitializer)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_env = env;
|
||||
_backgroundWorkerInitializer = backgroundWorkerInitializer;
|
||||
_abpBackgroundWorkerRecurringJobInitializer = abpBackgroundWorkerRecurringJobInitializer;
|
||||
}
|
||||
|
||||
[HttpGet("application-status")]
|
||||
|
|
@ -38,8 +44,24 @@ public class SetupController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost("migrate")]
|
||||
[AllowAnonymous]
|
||||
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.Headers["Cache-Control"] = "no-cache, no-store";
|
||||
Response.Headers["X-Accel-Buffering"] = "no";
|
||||
|
|
@ -141,7 +163,22 @@ public class SetupController : ControllerBase
|
|||
|
||||
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.");
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ namespace Sozsoft.Platform;
|
|||
/// </summary>
|
||||
internal static class SetupAppRunner
|
||||
{
|
||||
private const string SetupPasswordHeaderName = "X-Setup-Password";
|
||||
|
||||
// Veritabanı Hazırlık Kontrolü
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var csb = new SqlConnectionStringBuilder(connectionString);
|
||||
|
|
@ -175,6 +189,13 @@ internal static class SetupAppRunner
|
|||
app.MapPost("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env,
|
||||
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.Headers["Cache-Control"] = "no-cache, no-store";
|
||||
ctx.Response.Headers["X-Accel-Buffering"] = "no";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
|
|
@ -29,6 +30,12 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
|||
|
||||
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)
|
||||
{
|
||||
await next(context);
|
||||
|
|
@ -47,14 +54,22 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
|||
var tenantId = ParseTenantId(context.User.FindFirst(AbpClaimTypes.TenantId)?.Value);
|
||||
using (currentTenant.Change(tenantId))
|
||||
{
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await sessionRepository.GetListAsync(userId: userId);
|
||||
var isCurrentSession = sessions.Any(session => session.SessionId == sessionId);
|
||||
await uow.CompleteAsync();
|
||||
|
||||
if (!isCurrentSession)
|
||||
try
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||
var sessions = await sessionRepository.GetListAsync(userId: userId);
|
||||
var isCurrentSession = sessions.Any(session => session.SessionId == sessionId);
|
||||
await uow.CompleteAsync();
|
||||
|
||||
if (!isCurrentSession)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (IsDatabaseUnavailableException(ex))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,4 +81,13 @@ public class PlatformSessionValidationMiddleware : IMiddleware, ITransientDepend
|
|||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ public class DynamicGridReport : XtraReport
|
|||
localizer);
|
||||
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
|
||||
|
||||
ConfigurePage(reportColumns);
|
||||
ConfigurePage(reportColumns, isSubReport);
|
||||
ConfigureDataSource(
|
||||
selectQuery,
|
||||
connectionString,
|
||||
|
|
@ -162,13 +162,14 @@ public class DynamicGridReport : XtraReport
|
|||
}
|
||||
|
||||
#pragma warning disable CA1416
|
||||
private void ConfigurePage(IReadOnlyCollection<ReportColumn> visibleFields)
|
||||
private void ConfigurePage(IReadOnlyCollection<ReportColumn> visibleFields, bool isSubReport)
|
||||
{
|
||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
||||
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
||||
var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@
|
|||
"RefreshTokenMinutes": 90
|
||||
}
|
||||
},
|
||||
"Setup": {
|
||||
"MigrationPassword": "sozsoft"
|
||||
},
|
||||
"StringEncryption": {
|
||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,3 +17,9 @@ export const useDemo = () => {
|
|||
|
||||
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 { translate } = useLocalization()
|
||||
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 gridRef = useRef<DataGridRef>()
|
||||
|
|
@ -1096,7 +1100,7 @@ const Grid = (props: GridProps) => {
|
|||
})
|
||||
|
||||
return cols
|
||||
}, [gridDto])
|
||||
}, [gridDto, currentCulture, localizationTexts])
|
||||
|
||||
useEffect(() => {
|
||||
setColumnData(memoizedColumns)
|
||||
|
|
|
|||
|
|
@ -258,6 +258,10 @@ const Tree = (props: TreeProps) => {
|
|||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||
const { translate } = useLocalization()
|
||||
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 gridRef = useRef<TreeListRef>()
|
||||
|
|
@ -971,7 +975,7 @@ const Tree = (props: TreeProps) => {
|
|||
cols,
|
||||
)
|
||||
setTreeListDataSource(dataSource)
|
||||
}, [gridDto, searchParams])
|
||||
}, [gridDto, searchParams, currentCulture, localizationTexts])
|
||||
|
||||
useEffect(() => {
|
||||
const activeFilters = extraFilters.filter((f) => f.value)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { DesignerSelection } from './designer/types'
|
|||
import { useDesignerState } from './designer/useDesignerState'
|
||||
import { getHome, HomeDto, saveHomePage } from '@/services/home.service'
|
||||
import { Button, Notification, toast } from '@/components/ui'
|
||||
import { useDemo } from '@/contexts/DemoContext'
|
||||
import { useOptionalDemo } from '@/contexts/DemoContext'
|
||||
|
||||
interface HomeSlideServiceContent {
|
||||
icon: string
|
||||
|
|
@ -273,7 +273,7 @@ function buildHomeContent(home: HomeDto | undefined, translate: (key: string) =>
|
|||
}
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { openDemo } = useDemo()
|
||||
const { openDemo } = useOptionalDemo()
|
||||
const { translate } = useLocalization()
|
||||
const { setLang } = useStoreActions((actions) => actions.locale)
|
||||
const { getConfig } = useStoreActions((actions) => actions.abpConfig)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { getMigrateUrl, getSetupStatus } from '@/services/setup.service'
|
||||
import { applicationConfigurationUrl } from '@/services/abpConfig.service'
|
||||
import { Button } from '@/components/ui'
|
||||
import { Button, Dialog } from '@/components/ui'
|
||||
|
||||
interface LogLine {
|
||||
level: 'info' | 'warn' | 'error' | 'success' | 'restart' | 'done'
|
||||
|
|
@ -79,6 +79,8 @@ const DatabaseSetup = () => {
|
|||
const [status, setStatus] = useState<MigrationStatus>('idle')
|
||||
const [pollCountdown, setPollCountdown] = useState(0)
|
||||
const [dbExists, setDbExists] = useState<boolean | null>(null)
|
||||
const [setupPassword, setSetupPassword] = useState('')
|
||||
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false)
|
||||
const logEndRef = useRef<HTMLDivElement>(null)
|
||||
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
|
|
@ -119,9 +121,15 @@ const DatabaseSetup = () => {
|
|||
|
||||
const startMigration = async () => {
|
||||
if (status === 'running') return
|
||||
if (!setupPassword.trim()) {
|
||||
setStatus('error')
|
||||
addLog('error', 'Setup password is required.')
|
||||
return
|
||||
}
|
||||
|
||||
setLogs([])
|
||||
setStatus('running')
|
||||
setIsPasswordDialogOpen(false)
|
||||
addLog('info', 'Starting migration...')
|
||||
|
||||
const url = getMigrateUrl()
|
||||
|
|
@ -152,6 +160,7 @@ const DatabaseSetup = () => {
|
|||
|
||||
if (level === 'success') {
|
||||
setStatus('success')
|
||||
pollUntilReady()
|
||||
} else if (level === 'error') {
|
||||
setStatus('error')
|
||||
} else if (level === 'restart') {
|
||||
|
|
@ -166,12 +175,20 @@ const DatabaseSetup = () => {
|
|||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
headers: {
|
||||
Accept: 'text/event-stream',
|
||||
'X-Setup-Password': setupPassword,
|
||||
},
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
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')
|
||||
return
|
||||
}
|
||||
|
|
@ -196,9 +213,29 @@ const DatabaseSetup = () => {
|
|||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex flex-col items-center justify-center p-6 text-white">
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold mb-2">{APP_NAME}</h1>
|
||||
<p className="text-gray-400 text-lg">Initial Setup — Creating Database</p>
|
||||
<div className="mb-10 text-center sm:text-left">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<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>
|
||||
|
||||
{/* Status Card */}
|
||||
|
|
@ -234,7 +271,7 @@ const DatabaseSetup = () => {
|
|||
</div>
|
||||
|
||||
{/* 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">
|
||||
{status === 'idle' && dbExists === true && 'Database already exists. Migration is not required.'}
|
||||
{status === 'idle' && dbExists === false && 'Database not found. Press the button to start migration.'}
|
||||
|
|
@ -251,9 +288,10 @@ const DatabaseSetup = () => {
|
|||
{(status === 'idle' || status === 'error') && !dbExists && (
|
||||
<Button
|
||||
variant='solid'
|
||||
onClick={startMigration}
|
||||
className="min-w-40"
|
||||
onClick={() => setIsPasswordDialogOpen(true)}
|
||||
>
|
||||
{status === 'error' ? 'Retry' : 'Start Setup'}
|
||||
{status === 'error' ? 'Retry Migration' : 'Migrate'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
|
@ -267,6 +305,59 @@ const DatabaseSetup = () => {
|
|||
</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">
|
||||
This page is only visible when the database does not exist.
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue