This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal, Play } from 'lucide-react';
|
||||
import { Button, Card, Modal } from '../../components/ui';
|
||||
import { useToast } from '../../components/toast';
|
||||
@@ -42,6 +42,18 @@ function migrationTime(job: RuntimeControlJob) {
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
function sanitizeK8sName(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
function uniqueValues(values: string[]) {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
|
||||
const migrationPageSize = 5;
|
||||
|
||||
export function MigrationManagementPage() {
|
||||
@@ -64,6 +76,18 @@ export function MigrationManagementPage() {
|
||||
const selectedRelease = releases.find((release) => release.id === releaseId);
|
||||
const migrationComponents = selectedApp?.components?.filter((component) => component.type === 'MIGRATION') ?? [];
|
||||
const images = imageEntries(selectedRelease?.imageTags);
|
||||
const namespaceOptions = useMemo(
|
||||
() => uniqueValues([selectedApp?.namespace || '', namespace]),
|
||||
[namespace, selectedApp?.namespace]
|
||||
);
|
||||
const envSecretOptions = useMemo(
|
||||
() => uniqueValues([
|
||||
...images.map(([name]) => `${sanitizeK8sName(name)}-secret`),
|
||||
`${sanitizeK8sName(selectedApp?.key ?? 'app')}-be-secret`,
|
||||
envSecretName
|
||||
]),
|
||||
[envSecretName, images, selectedApp?.key]
|
||||
);
|
||||
const selectedImage = selectedImageTag(selectedRelease, imageComponentName);
|
||||
const sortedJobs = [...jobs].sort((a, b) => migrationTime(b) - migrationTime(a));
|
||||
const totalPages = Math.max(1, Math.ceil(sortedJobs.length / migrationPageSize));
|
||||
@@ -83,7 +107,7 @@ export function MigrationManagementPage() {
|
||||
setMigrationComponentId((prev) => (
|
||||
migrationComponents.some((component) => component.id === prev) ? prev : migrationComponents[0]?.id || ''
|
||||
));
|
||||
setEnvSecretName((prev) => prev || `${selectedApp?.key ?? 'app'}-be-secret`);
|
||||
setEnvSecretName((prev) => prev || `${sanitizeK8sName(selectedApp?.key ?? 'app')}-be-secret`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -98,10 +122,14 @@ export function MigrationManagementPage() {
|
||||
setImageComponentName((prev) => (nextImages.some(([name]) => name === prev) ? prev : nextImages[0]?.[0] || ''));
|
||||
}, [selectedRelease?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageComponentName) setEnvSecretName(`${sanitizeK8sName(imageComponentName)}-secret`);
|
||||
}, [imageComponentName]);
|
||||
|
||||
useEffect(() => {
|
||||
setNamespace(selectedApp?.namespace || '');
|
||||
setMigrationComponentId(migrationComponents[0]?.id || '');
|
||||
setEnvSecretName(`${selectedApp?.key ?? 'app'}-be-secret`);
|
||||
setEnvSecretName(`${sanitizeK8sName(selectedApp?.key ?? 'app')}-be-secret`);
|
||||
setPage(1);
|
||||
}, [selectedAppId]);
|
||||
|
||||
@@ -117,7 +145,7 @@ export function MigrationManagementPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const canRun = Boolean(releaseId && migrationComponentId && imageComponentName && namespace && command.trim());
|
||||
const canRun = Boolean(releaseId && migrationComponentId && imageComponentName && namespace && envSecretName && command.trim());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -182,19 +210,25 @@ export function MigrationManagementPage() {
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">네임스페이스</span>
|
||||
<input
|
||||
<select
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
value={namespace}
|
||||
onChange={(event) => setNamespace(event.target.value)}
|
||||
/>
|
||||
>
|
||||
{namespaceOptions.length === 0 ? <option value="">네임스페이스 없음</option> : null}
|
||||
{namespaceOptions.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-muted-foreground">envFrom Secret</span>
|
||||
<input
|
||||
<select
|
||||
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-ring"
|
||||
value={envSecretName}
|
||||
onChange={(event) => setEnvSecretName(event.target.value)}
|
||||
/>
|
||||
>
|
||||
{envSecretOptions.length === 0 ? <option value="">Secret 없음</option> : null}
|
||||
{envSecretOptions.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block lg:col-span-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">명령</span>
|
||||
|
||||
Reference in New Issue
Block a user