Improve logging recovery, resource scaling, and app deploy logging.
Auto-reconnect Elasticsearch port-forward after cluster or API restarts, poll log status in the UI, and apply storage changes through billing upgrade for all workloads. Add Redis/RabbitMQ PVC resize, Helm ES credentials for Fluent Bit, and fix deploy progress overlay behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,7 +21,7 @@ export function DeletingTableRowCell({
|
||||
message?: string;
|
||||
}) {
|
||||
return (
|
||||
<td colSpan={colSpan} className="px-6 py-4 bg-white/95" aria-live="polite" aria-busy="true">
|
||||
<td colSpan={colSpan} className="px-6 py-4 bg-white/80 backdrop-blur-sm" aria-live="polite" aria-busy="true">
|
||||
<div className="flex min-h-[52px] w-full items-center justify-center gap-2 text-sm font-semibold text-gray-800">
|
||||
<Clock className="w-4 h-4 shrink-0 animate-spin text-red-600" />
|
||||
<span>{message}</span>
|
||||
@@ -34,7 +34,7 @@ export function DeletingTableRowCell({
|
||||
export function DeletingCardOverlay({ message = 'Deleting…' }: { message?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/92 backdrop-blur-[2px] text-sm font-semibold text-gray-800"
|
||||
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/75 backdrop-blur-sm text-sm font-semibold text-gray-800"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
import { useQuery, useQueries, keepPreviousData } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import type { Application } from '@/types';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
@@ -49,16 +49,15 @@ export function DeploymentProgressManager() {
|
||||
.get<{ progress: BuildProgress | null }>(`/deployments/applications/${app.id}/build-progress`)
|
||||
.then((r) => r.data.progress),
|
||||
refetchInterval: 1500,
|
||||
placeholderData: keepPreviousData,
|
||||
})),
|
||||
});
|
||||
|
||||
const activeItems = useMemo(() => {
|
||||
return deployingApps
|
||||
.map((app, i) => ({
|
||||
app,
|
||||
progress: progressQueries[i]?.data ?? null,
|
||||
}))
|
||||
.filter(({ progress }) => isActiveBuildProgress(progress));
|
||||
return deployingApps.map((app, i) => ({
|
||||
app,
|
||||
progress: progressQueries[i]?.data ?? null,
|
||||
}));
|
||||
}, [deployingApps, progressQueries]);
|
||||
|
||||
const routeAppId =
|
||||
@@ -66,10 +65,10 @@ export function DeploymentProgressManager() {
|
||||
pathname.match(/\/dashboard\/services\/([^/]+)/)?.[1];
|
||||
|
||||
useEffect(() => {
|
||||
if (activeItems.length === 0) {
|
||||
if (deployingApps.length === 0) {
|
||||
useDeployProgressStore.setState({ minimized: false, focusedAppId: null });
|
||||
}
|
||||
}, [activeItems.length]);
|
||||
}, [deployingApps.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeItems.length === 0) return;
|
||||
@@ -84,12 +83,15 @@ export function DeploymentProgressManager() {
|
||||
const focusedItem =
|
||||
activeItems.find((item) => item.app.id === focusedAppId) ?? activeItems[0];
|
||||
|
||||
const focusedProgress = focusedItem?.progress;
|
||||
const focusedProgress =
|
||||
focusedItem?.progress ??
|
||||
({ phase: 'building', percent: 0, message: 'Loading progress…' } satisfies BuildProgress);
|
||||
const showModal =
|
||||
!minimized &&
|
||||
!!focusedItem &&
|
||||
!!focusedProgress &&
|
||||
isActiveBuildProgress(focusedProgress);
|
||||
(!focusedItem.progress ||
|
||||
isActiveBuildProgress(focusedItem.progress) ||
|
||||
focusedItem.progress.phase === 'done');
|
||||
|
||||
const handleMinimize = () => {
|
||||
if (focusedItem) minimize(focusedItem.app.id);
|
||||
@@ -99,7 +101,7 @@ export function DeploymentProgressManager() {
|
||||
expand(appId);
|
||||
};
|
||||
|
||||
if (activeItems.length === 0) return null;
|
||||
if (deployingApps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -109,7 +111,7 @@ export function DeploymentProgressManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && focusedProgress && (
|
||||
{showModal && (
|
||||
<BuildProgressModal
|
||||
appId={focusedItem.app.id}
|
||||
appName={focusedItem.app.name}
|
||||
|
||||
@@ -226,23 +226,6 @@ export function ManagedServiceResourcesPanel({
|
||||
},
|
||||
});
|
||||
|
||||
const resizeDbMutation = useMutation({
|
||||
mutationFn: (size: string) => api.patch(`/applications/${serviceId}/db-storage`, { size }),
|
||||
onSuccess: (res) => {
|
||||
if (res.data.success) {
|
||||
toast.success(res.data.message || 'Storage expanded');
|
||||
queryClient.invalidateQueries({ queryKey: ['db-storage', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['storage-usage', serviceId] });
|
||||
} else {
|
||||
toast.error(res.data.message || 'Failed to expand storage');
|
||||
}
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'Failed to resize storage');
|
||||
},
|
||||
});
|
||||
|
||||
const dbUploadMutation = useMutation({
|
||||
mutationFn: (file: File) => {
|
||||
const formData = new FormData();
|
||||
@@ -280,9 +263,15 @@ export function ManagedServiceResourcesPanel({
|
||||
[dbUploadMutation],
|
||||
);
|
||||
|
||||
const currentDbGi =
|
||||
parseInt((dbStorageData?.currentSize || app.dbStorageSize || '1Gi').replace('Gi', ''), 10) || 1;
|
||||
|
||||
const buildUpgradePayload = useCallback((): UpgradePayload => {
|
||||
if (app.productType === 'managed_database') {
|
||||
return { ...dbResources };
|
||||
const payload: UpgradePayload = { ...dbResources };
|
||||
const newGi = parseInt(dbStorageSize, 10);
|
||||
if (newGi > currentDbGi) payload.dbStorageSize = `${newGi}Gi`;
|
||||
return payload;
|
||||
}
|
||||
if (app.productType === 'managed_redis') {
|
||||
return {
|
||||
@@ -304,7 +293,7 @@ export function ManagedServiceResourcesPanel({
|
||||
storageGi: rabbitResources.storageGi ?? app.optionalServiceResources?.rabbitmq?.storageGi ?? 2,
|
||||
},
|
||||
};
|
||||
}, [app, dbResources, redisResources, rabbitResources]);
|
||||
}, [app, dbResources, redisResources, rabbitResources, dbStorageSize, currentDbGi]);
|
||||
|
||||
const applyResources = () => {
|
||||
if (needsRenewal) {
|
||||
@@ -316,6 +305,10 @@ export function ManagedServiceResourcesPanel({
|
||||
const w = workloadKey(app);
|
||||
|
||||
if (!app.billingCycle) {
|
||||
if (Object.keys(payload).length > 0 && (payload.dbStorageSize || payload.redisResources || payload.rabbitmqResources)) {
|
||||
scaleMutation.mutate(payload);
|
||||
return;
|
||||
}
|
||||
if (w === 'database') {
|
||||
directPatchResourcesMutation.mutate({ workload: 'database', ...dbResources });
|
||||
} else if (w === 'redis') {
|
||||
@@ -351,37 +344,12 @@ export function ManagedServiceResourcesPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandStorage = () => {
|
||||
if (needsRenewal) {
|
||||
toast.warn('Renew the service before expanding storage');
|
||||
return;
|
||||
}
|
||||
const newGi = parseInt(dbStorageSize, 10);
|
||||
if (newGi <= currentDbGi) {
|
||||
toast.warn('New size must be larger than current allocation');
|
||||
return;
|
||||
}
|
||||
const newSize = `${newGi}Gi`;
|
||||
const payload: UpgradePayload = { dbStorageSize: newSize };
|
||||
|
||||
if (!app.billingCycle) {
|
||||
resizeDbMutation.mutate(newSize);
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingUpgradePayload(payload);
|
||||
calculateUpgradeCostMutation.mutate(payload);
|
||||
};
|
||||
|
||||
const resourcesPending =
|
||||
directPatchResourcesMutation.isPending ||
|
||||
scaleMutation.isPending ||
|
||||
calculateUpgradeCostMutation.isPending ||
|
||||
createUpgradeInvoiceMutation.isPending;
|
||||
|
||||
const currentDbGi =
|
||||
parseInt((dbStorageData?.currentSize || app.dbStorageSize || '1Gi').replace('Gi', ''), 10) || 1;
|
||||
|
||||
const metricsWorkload =
|
||||
resourceUsage?.workloads?.find((w) => w.key === workloadKey(app)) ||
|
||||
(resourceUsage?.configured
|
||||
@@ -558,22 +526,8 @@ export function ManagedServiceResourcesPanel({
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-600">GB</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary text-xs px-3 py-1.5 disabled:opacity-50"
|
||||
disabled={
|
||||
resourcesPending ||
|
||||
!isDeployed ||
|
||||
parseInt(dbStorageSize, 10) <= currentDbGi
|
||||
}
|
||||
onClick={handleExpandStorage}
|
||||
>
|
||||
{resizeDbMutation.isPending || calculateUpgradeCostMutation.isPending
|
||||
? 'Expanding…'
|
||||
: 'Expand'}
|
||||
</button>
|
||||
<p className="text-xs text-gray-400 w-full">
|
||||
Only expansion is allowed.
|
||||
Only expansion is allowed. Use Apply changes below.
|
||||
{app.billingCycle
|
||||
? ' Additional storage is charged for the remaining billing period.'
|
||||
: ''}
|
||||
|
||||
Reference in New Issue
Block a user