Merge branch 'main' into refactor/frontend-time-format-options-20260813
This commit is contained in:
@@ -26,6 +26,9 @@ type FixtureState = {
|
|||||||
status: OperationStatus;
|
status: OperationStatus;
|
||||||
sourceMode?: 'BRANCH' | 'COMMIT';
|
sourceMode?: 'BRANCH' | 'COMMIT';
|
||||||
sourceRef?: string;
|
sourceRef?: string;
|
||||||
|
resolvedCommitSha?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
error?: string;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
requestedBy: string;
|
requestedBy: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -38,6 +41,9 @@ type FixtureState = {
|
|||||||
profileLogsEmpty?: boolean;
|
profileLogsEmpty?: boolean;
|
||||||
gatewayLogPollCount?: number;
|
gatewayLogPollCount?: number;
|
||||||
gatewayLogsEmpty?: boolean;
|
gatewayLogsEmpty?: boolean;
|
||||||
|
gatewayStateFailuresAfterRequest?: number;
|
||||||
|
gatewayStateFailuresRemaining?: number;
|
||||||
|
gatewayStateFailureCount?: number;
|
||||||
capabilities?: Array<{ permission: string; scope: 'GLOBAL' | 'PROFILE'; scopes: string[] }>;
|
capabilities?: Array<{ permission: string; scope: 'GLOBAL' | 'PROFILE'; scopes: string[] }>;
|
||||||
profileListDelayMs?: number;
|
profileListDelayMs?: number;
|
||||||
profileNavigationDelayMs?: number;
|
profileNavigationDelayMs?: number;
|
||||||
@@ -139,6 +145,19 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
await route.abort('failed');
|
await route.abort('failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
names.includes('admin.releases.gatewayState') &&
|
||||||
|
(state.gatewayStateFailuresRemaining ?? 0) > 0
|
||||||
|
) {
|
||||||
|
state.gatewayStateFailuresRemaining = (state.gatewayStateFailuresRemaining ?? 0) - 1;
|
||||||
|
state.gatewayStateFailureCount = (state.gatewayStateFailureCount ?? 0) + 1;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 502,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: '',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (names.includes('admin.operations.logs') && !state.profileLogProgress) {
|
if (names.includes('admin.operations.logs') && !state.profileLogProgress) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
}
|
}
|
||||||
@@ -337,6 +356,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
|||||||
updatedAt: '2026-08-01T02:00:00.000Z',
|
updatedAt: '2026-08-01T02:00:00.000Z',
|
||||||
};
|
};
|
||||||
state.gatewayOperations = [releaseOperation];
|
state.gatewayOperations = [releaseOperation];
|
||||||
|
state.gatewayStateFailuresRemaining = state.gatewayStateFailuresAfterRequest;
|
||||||
return response(releaseOperation);
|
return response(releaseOperation);
|
||||||
}
|
}
|
||||||
if (name === 'admin.operations.requestRuntime') {
|
if (name === 'admin.operations.requestRuntime') {
|
||||||
@@ -815,7 +835,13 @@ test('scenario-only operator resets the current version without Git or Gateway c
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('controls gateway deployment and rollback through the external controller queue', async ({ page }, testInfo) => {
|
test('controls gateway deployment and rollback through the external controller queue', async ({ page }, testInfo) => {
|
||||||
const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] };
|
const state: FixtureState = {
|
||||||
|
operations: [],
|
||||||
|
gatewayOperations: [],
|
||||||
|
runtimeRunning: true,
|
||||||
|
requestBodies: [],
|
||||||
|
gatewayStateFailuresAfterRequest: 1,
|
||||||
|
};
|
||||||
await installFixture(page, state);
|
await installFixture(page, state);
|
||||||
page.on('dialog', (dialog) => dialog.accept());
|
page.on('dialog', (dialog) => dialog.accept());
|
||||||
|
|
||||||
@@ -828,6 +854,9 @@ test('controls gateway deployment and rollback through the external controller q
|
|||||||
await page.getByTestId('request-gateway-deploy').click();
|
await page.getByTestId('request-gateway-deploy').click();
|
||||||
|
|
||||||
await expect(page.getByText(/Gateway 배포 작업을 등록했습니다/).first()).toBeVisible();
|
await expect(page.getByText(/Gateway 배포 작업을 등록했습니다/).first()).toBeVisible();
|
||||||
|
expect(state.gatewayStateFailureCount).toBe(1);
|
||||||
|
await expect(page.getByTestId('server-operations-page')).not.toContainText('Unexpected end of JSON input');
|
||||||
|
await expect(page.getByTestId('action-toast').filter({ hasText: 'Unexpected end of JSON input' })).toHaveCount(0);
|
||||||
await expect(page.getByTestId('gateway-release-table')).toContainText('DEPLOY');
|
await expect(page.getByTestId('gateway-release-table')).toContainText('DEPLOY');
|
||||||
await expect(page.getByTestId('gateway-release-log-panel')).toBeVisible();
|
await expect(page.getByTestId('gateway-release-log-panel')).toBeVisible();
|
||||||
await expect(page.getByTestId('gateway-release-log')).toContainText('Gateway 구성 요소를 빌드합니다.');
|
await expect(page.getByTestId('gateway-release-log')).toContainText('Gateway 구성 요소를 빌드합니다.');
|
||||||
@@ -853,6 +882,131 @@ test('controls gateway deployment and rollback through the external controller q
|
|||||||
expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.requestGatewayRollback')).toBe(true);
|
expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.requestGatewayRollback')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('moves long Gateway release errors out of the table column into an expandable detail row', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const longError = [
|
||||||
|
'Gateway release did not become ready before the timeout.',
|
||||||
|
'Error: gateway-frontend readiness check failed after 30 attempts',
|
||||||
|
' at waitForGatewayReadiness (/srv/core/release-controller/dist/releaseController.js:842:19)',
|
||||||
|
'controller-output-without-breaks-'.repeat(12),
|
||||||
|
].join('\n');
|
||||||
|
const operationId = '88888888-8888-4888-8888-888888888888';
|
||||||
|
const state: FixtureState = {
|
||||||
|
operations: [],
|
||||||
|
gatewayOperations: [
|
||||||
|
{
|
||||||
|
id: operationId,
|
||||||
|
type: 'DEPLOY',
|
||||||
|
status: 'FAILED',
|
||||||
|
sourceMode: 'COMMIT',
|
||||||
|
sourceRef: 'cccccccccccccccccccccccccccccccccccccccc',
|
||||||
|
resolvedCommitSha: 'cccccccccccccccccccccccccccccccccccccccc',
|
||||||
|
completedAt: '2026-08-01T02:03:00.000Z',
|
||||||
|
error: longError,
|
||||||
|
payload: {},
|
||||||
|
requestedBy: 'admin',
|
||||||
|
createdAt: '2026-08-01T02:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-01T02:03:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
gatewayLogsEmpty: true,
|
||||||
|
runtimeRunning: true,
|
||||||
|
requestBodies: [],
|
||||||
|
};
|
||||||
|
await installFixture(page, state);
|
||||||
|
|
||||||
|
await page.goto('admin/releases');
|
||||||
|
const table = page.getByTestId('gateway-release-table');
|
||||||
|
await expect(table.getByRole('columnheader')).toHaveCount(6);
|
||||||
|
await expect(table.getByRole('columnheader', { name: '오류', exact: true })).toHaveCount(0);
|
||||||
|
await expect(table.getByRole('columnheader', { name: '상세', exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
const errorToggle = page.getByTestId('gateway-release-error-toggle');
|
||||||
|
await expect(errorToggle).toHaveText('오류 보기');
|
||||||
|
await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();
|
||||||
|
|
||||||
|
await errorToggle.focus();
|
||||||
|
const focusedToggleStyle = await errorToggle.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
outlineStyle: style.outlineStyle,
|
||||||
|
outlineWidth: style.outlineWidth,
|
||||||
|
color: style.color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(focusedToggleStyle.outlineStyle).not.toBe('none');
|
||||||
|
expect(parseFloat(focusedToggleStyle.outlineWidth)).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
await errorToggle.hover();
|
||||||
|
await errorToggle.click();
|
||||||
|
await expect(errorToggle).toHaveText('오류 닫기');
|
||||||
|
await expect(errorToggle).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
const errorDetail = page.getByTestId('gateway-release-error-detail');
|
||||||
|
await expect(errorDetail).toContainText('Gateway release did not become ready');
|
||||||
|
await expect(errorDetail).toContainText('controller-output-without-breaks');
|
||||||
|
const desktopGeometry = await table.evaluate((element) => {
|
||||||
|
const headings = Array.from(element.querySelectorAll('thead th'));
|
||||||
|
const detail = element.querySelector('[data-testid="gateway-release-error-detail"]');
|
||||||
|
const detailCell = detail?.querySelector('td');
|
||||||
|
const errorText = detail?.querySelector('pre');
|
||||||
|
return {
|
||||||
|
tableWidth: element.getBoundingClientRect().width,
|
||||||
|
scrollerWidth: element.parentElement?.getBoundingClientRect().width ?? 0,
|
||||||
|
tableLayout: getComputedStyle(element).tableLayout,
|
||||||
|
columnCount: headings.length,
|
||||||
|
detailColSpan: detailCell?.getAttribute('colspan'),
|
||||||
|
detailWidth: detailCell?.getBoundingClientRect().width ?? 0,
|
||||||
|
errorWhiteSpace: errorText ? getComputedStyle(errorText).whiteSpace : '',
|
||||||
|
errorOverflowWrap: errorText ? getComputedStyle(errorText).overflowWrap : '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(desktopGeometry).toMatchObject({
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
columnCount: 6,
|
||||||
|
detailColSpan: '6',
|
||||||
|
errorWhiteSpace: 'pre-wrap',
|
||||||
|
});
|
||||||
|
expect(desktopGeometry.tableWidth).toBeGreaterThanOrEqual(680);
|
||||||
|
expect(desktopGeometry.detailWidth).toBeGreaterThanOrEqual(desktopGeometry.tableWidth - 1);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-desktop.png'), fullPage: true });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(table.getByRole('columnheader')).toHaveCount(4);
|
||||||
|
await expect(table.getByRole('columnheader', { name: '소스', exact: true })).toHaveCount(0);
|
||||||
|
await expect(table.getByRole('columnheader', { name: '해석 커밋', exact: true })).toHaveCount(0);
|
||||||
|
const mobileGeometry = await table.evaluate((element) => {
|
||||||
|
const scroller = element.parentElement!;
|
||||||
|
const scrollerRect = scroller.getBoundingClientRect();
|
||||||
|
const detailRect = element
|
||||||
|
.querySelector('[data-testid="gateway-release-error-detail"]')!
|
||||||
|
.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
tableWidth: element.getBoundingClientRect().width,
|
||||||
|
scrollerX: scrollerRect.x,
|
||||||
|
scrollerWidth: scrollerRect.width,
|
||||||
|
scrollerScrollWidth: scroller.scrollWidth,
|
||||||
|
detailWidth: detailRect.width,
|
||||||
|
viewportWidth: document.documentElement.clientWidth,
|
||||||
|
documentScrollWidth: document.documentElement.scrollWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(mobileGeometry.tableWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
|
||||||
|
expect(mobileGeometry.detailWidth).toBeGreaterThanOrEqual(mobileGeometry.tableWidth - 1);
|
||||||
|
expect(mobileGeometry.scrollerScrollWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
|
||||||
|
expect(mobileGeometry.scrollerX).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(mobileGeometry.scrollerX + mobileGeometry.scrollerWidth).toBeLessThanOrEqual(
|
||||||
|
mobileGeometry.viewportWidth
|
||||||
|
);
|
||||||
|
expect(mobileGeometry.documentScrollWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-mobile.png'), fullPage: true });
|
||||||
|
|
||||||
|
await errorToggle.click();
|
||||||
|
await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
test('explains terminal releases created before controller progress logging', async ({ page }) => {
|
test('explains terminal releases created before controller progress logging', async ({ page }) => {
|
||||||
const state: FixtureState = {
|
const state: FixtureState = {
|
||||||
operations: [],
|
operations: [],
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const profileOperationLogViewport = ref<HTMLElement>();
|
|||||||
const gatewayReleaseState = ref<GatewayReleaseState | null>(null);
|
const gatewayReleaseState = ref<GatewayReleaseState | null>(null);
|
||||||
const gatewayReleaseOperations = ref<GatewayReleaseOperation[]>([]);
|
const gatewayReleaseOperations = ref<GatewayReleaseOperation[]>([]);
|
||||||
const selectedGatewayOperationId = ref('');
|
const selectedGatewayOperationId = ref('');
|
||||||
|
const expandedGatewayErrorOperationId = ref('');
|
||||||
const gatewayReleaseLogs = ref<GatewayReleaseLog[]>([]);
|
const gatewayReleaseLogs = ref<GatewayReleaseLog[]>([]);
|
||||||
const gatewayReleaseLogCursor = ref<string>();
|
const gatewayReleaseLogCursor = ref<string>();
|
||||||
const gatewayReleaseLogStatus = ref('');
|
const gatewayReleaseLogStatus = ref('');
|
||||||
@@ -113,6 +114,7 @@ let stateRequestInFlight = false;
|
|||||||
let releaseLogLoopGeneration = 0;
|
let releaseLogLoopGeneration = 0;
|
||||||
let profileLogLoopGeneration = 0;
|
let profileLogLoopGeneration = 0;
|
||||||
let componentMounted = false;
|
let componentMounted = false;
|
||||||
|
let gatewayReleaseTransitionActive = false;
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
sourceMode: (props.mode === 'scenario' ? 'CURRENT' : 'BRANCH') as 'CURRENT' | 'BRANCH' | 'COMMIT',
|
sourceMode: (props.mode === 'scenario' ? 'CURRENT' : 'BRANCH') as 'CURRENT' | 'BRANCH' | 'COMMIT',
|
||||||
@@ -169,7 +171,7 @@ const gatewayReleaseLogEmptyMessage = computed(() => {
|
|||||||
return 'controller 로그를 기다리고 있습니다…';
|
return 'controller 로그를 기다리고 있습니다…';
|
||||||
}
|
}
|
||||||
if (operation.error) {
|
if (operation.error) {
|
||||||
return `이 작업에는 controller 로그가 기록되지 않았습니다. 작업 오류: ${operation.error}`;
|
return '이 작업에는 controller 로그가 기록되지 않았습니다. 작업 이력의 오류 상세를 확인하세요.';
|
||||||
}
|
}
|
||||||
return '이 작업에는 controller 로그가 기록되지 않았습니다. 로그 지원 controller 적용 전 작업일 수 있습니다.';
|
return '이 작업에는 controller 로그가 기록되지 않았습니다. 로그 지원 controller 적용 전 작업일 수 있습니다.';
|
||||||
});
|
});
|
||||||
@@ -286,6 +288,7 @@ const loadState = async (quiet = false) => {
|
|||||||
const active = gatewayReleaseOperations.value.find((operation) =>
|
const active = gatewayReleaseOperations.value.find((operation) =>
|
||||||
['QUEUED', 'RUNNING'].includes(operation.status)
|
['QUEUED', 'RUNNING'].includes(operation.status)
|
||||||
);
|
);
|
||||||
|
gatewayReleaseTransitionActive = Boolean(active);
|
||||||
if (active && selectedGatewayOperationId.value !== active.id) {
|
if (active && selectedGatewayOperationId.value !== active.id) {
|
||||||
selectedGatewayOperationId.value = active.id;
|
selectedGatewayOperationId.value = active.id;
|
||||||
} else if (
|
} else if (
|
||||||
@@ -312,7 +315,11 @@ const loadState = async (quiet = false) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '운영 상태를 불러오지 못했습니다.';
|
// Gateway release가 process를 교체하는 동안에는 background poll의 HTTP 연결이
|
||||||
|
// 일시적으로 끊길 수 있다. 이 경우는 기존 상태를 유지하고 다음 poll에서 복구한다.
|
||||||
|
if (!quiet || props.mode !== 'gateway' || !gatewayReleaseTransitionActive) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '운영 상태를 불러오지 못했습니다.';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
stateRequestInFlight = false;
|
stateRequestInFlight = false;
|
||||||
@@ -429,6 +436,11 @@ const selectGatewayReleaseOperation = (operationId: string) => {
|
|||||||
selectedGatewayOperationId.value = operationId;
|
selectedGatewayOperationId.value = operationId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleGatewayReleaseError = (operationId: string) => {
|
||||||
|
expandedGatewayErrorOperationId.value =
|
||||||
|
expandedGatewayErrorOperationId.value === operationId ? '' : operationId;
|
||||||
|
};
|
||||||
|
|
||||||
const requestDeploy = async () => {
|
const requestDeploy = async () => {
|
||||||
clearStatus();
|
clearStatus();
|
||||||
if (
|
if (
|
||||||
@@ -475,6 +487,7 @@ const requestGatewayDeploy = async () => {
|
|||||||
sourceRef: gatewayForm.sourceRef.trim(),
|
sourceRef: gatewayForm.sourceRef.trim(),
|
||||||
reason: gatewayForm.reason.trim() || undefined,
|
reason: gatewayForm.reason.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
gatewayReleaseTransitionActive = true;
|
||||||
selectedGatewayOperationId.value = operation.id;
|
selectedGatewayOperationId.value = operation.id;
|
||||||
message.value = 'Gateway 배포 작업을 등록했습니다. 외부 release-controller가 처리합니다.';
|
message.value = 'Gateway 배포 작업을 등록했습니다. 외부 release-controller가 처리합니다.';
|
||||||
await loadState(true);
|
await loadState(true);
|
||||||
@@ -499,6 +512,7 @@ const requestGatewayRollback = async () => {
|
|||||||
const operation = await adminClient.releases.requestGatewayRollback.mutate({
|
const operation = await adminClient.releases.requestGatewayRollback.mutate({
|
||||||
reason: gatewayForm.reason.trim() || undefined,
|
reason: gatewayForm.reason.trim() || undefined,
|
||||||
});
|
});
|
||||||
|
gatewayReleaseTransitionActive = true;
|
||||||
selectedGatewayOperationId.value = operation.id;
|
selectedGatewayOperationId.value = operation.id;
|
||||||
message.value = 'Gateway rollback 작업을 등록했습니다.';
|
message.value = 'Gateway rollback 작업을 등록했습니다.';
|
||||||
await loadState(true);
|
await loadState(true);
|
||||||
@@ -1119,45 +1133,97 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full min-w-[760px] text-left text-xs" data-testid="gateway-release-table">
|
<table
|
||||||
|
class="w-full table-fixed text-left text-xs sm:min-w-[680px]"
|
||||||
|
data-testid="gateway-release-table"
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col class="w-[32%] sm:w-[144px]" />
|
||||||
|
<col class="w-[17%] sm:w-[68px]" />
|
||||||
|
<col class="w-[20%] sm:w-[88px]" />
|
||||||
|
<col class="hidden sm:table-column sm:w-[128px]" />
|
||||||
|
<col class="hidden sm:table-column sm:w-[112px]" />
|
||||||
|
<col class="w-[31%] sm:w-[140px]" />
|
||||||
|
</colgroup>
|
||||||
<thead class="border-b border-zinc-700 text-zinc-500">
|
<thead class="border-b border-zinc-700 text-zinc-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="p-2">시각</th>
|
<th class="p-2">시각</th>
|
||||||
<th class="p-2">작업</th>
|
<th class="p-2">작업</th>
|
||||||
<th class="p-2">상태</th>
|
<th class="p-2">상태</th>
|
||||||
<th class="p-2">소스</th>
|
<th class="hidden p-2 sm:table-cell">소스</th>
|
||||||
<th class="p-2">해석 커밋</th>
|
<th class="hidden p-2 sm:table-cell">해석 커밋</th>
|
||||||
<th class="p-2">오류</th>
|
<th class="p-2">상세</th>
|
||||||
<th class="p-2">로그</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<template v-for="operation in gatewayReleaseOperations" :key="operation.id">
|
||||||
v-for="operation in gatewayReleaseOperations"
|
<tr class="border-b border-zinc-800 align-top">
|
||||||
:key="operation.id"
|
<td class="p-2">{{ formatTime(operation.createdAt) }}</td>
|
||||||
class="border-b border-zinc-800"
|
<td class="p-2">
|
||||||
>
|
<div>{{ operation.type }}</div>
|
||||||
<td class="p-2">{{ formatTime(operation.createdAt) }}</td>
|
<div
|
||||||
<td class="p-2">{{ operation.type }}</td>
|
class="mt-1 truncate font-mono text-[10px] text-zinc-500 sm:hidden"
|
||||||
<td class="p-2">{{ operation.status }}</td>
|
:title="operation.sourceRef"
|
||||||
<td class="p-2 font-mono">{{ operation.sourceRef }}</td>
|
>
|
||||||
<td class="p-2 font-mono">{{ shortSha(operation.resolvedCommitSha) }}</td>
|
{{ operation.sourceRef ?? '-' }}
|
||||||
<td class="max-w-xs p-2 text-red-300">{{ operation.error }}</td>
|
</div>
|
||||||
<td class="p-2">
|
</td>
|
||||||
<button
|
<td class="p-2 font-semibold">{{ operation.status }}</td>
|
||||||
type="button"
|
<td class="hidden p-2 font-mono sm:table-cell">
|
||||||
class="rounded border border-zinc-700 px-2 py-1 text-zinc-300 hover:bg-zinc-800"
|
<div class="truncate" :title="operation.sourceRef">{{ operation.sourceRef }}</div>
|
||||||
:class="
|
</td>
|
||||||
operation.id === selectedGatewayOperationId
|
<td class="hidden p-2 font-mono sm:table-cell">
|
||||||
? 'border-violet-500 text-violet-200'
|
{{ shortSha(operation.resolvedCommitSha) }}
|
||||||
: ''
|
</td>
|
||||||
"
|
<td class="p-2">
|
||||||
@click="selectGatewayReleaseOperation(operation.id)"
|
<div class="flex flex-wrap gap-2">
|
||||||
>
|
<button
|
||||||
보기
|
type="button"
|
||||||
</button>
|
class="rounded border border-zinc-700 px-2 py-1 text-zinc-300 hover:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-400"
|
||||||
</td>
|
:class="
|
||||||
</tr>
|
operation.id === selectedGatewayOperationId
|
||||||
|
? 'border-violet-500 text-violet-200'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="selectGatewayReleaseOperation(operation.id)"
|
||||||
|
>
|
||||||
|
로그
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="operation.error"
|
||||||
|
type="button"
|
||||||
|
class="rounded border border-red-800 px-2 py-1 text-red-300 hover:bg-red-950 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-400"
|
||||||
|
:aria-expanded="expandedGatewayErrorOperationId === operation.id"
|
||||||
|
:aria-controls="`gateway-release-error-${operation.id}`"
|
||||||
|
data-testid="gateway-release-error-toggle"
|
||||||
|
@click="toggleGatewayReleaseError(operation.id)"
|
||||||
|
>
|
||||||
|
{{ expandedGatewayErrorOperationId === operation.id ? '오류 닫기' : '오류 보기' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-if="operation.error"
|
||||||
|
v-show="expandedGatewayErrorOperationId === operation.id"
|
||||||
|
:id="`gateway-release-error-${operation.id}`"
|
||||||
|
class="border-b border-red-900/70 bg-red-950/30"
|
||||||
|
data-testid="gateway-release-error-detail"
|
||||||
|
>
|
||||||
|
<td colspan="6" class="p-4">
|
||||||
|
<div
|
||||||
|
class="rounded border border-red-900/70 bg-zinc-950 px-4 py-3"
|
||||||
|
role="region"
|
||||||
|
:aria-label="`${operation.type} 릴리스 오류 상세`"
|
||||||
|
>
|
||||||
|
<div class="mb-2 text-xs font-semibold text-red-300">오류 상세</div>
|
||||||
|
<pre
|
||||||
|
class="whitespace-pre-wrap break-all font-mono text-xs leading-5 text-red-200"
|
||||||
|
>{{ operation.error }}</pre>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user