причесываем лог
This commit is contained in:
@@ -240,8 +240,10 @@ export const api = {
|
||||
getEnvironmentsList: () => fetchApi('/environments'),
|
||||
getLlmStatus: () => fetchApi('/llm/status'),
|
||||
getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),
|
||||
|
||||
// Dashboards
|
||||
getStorageFileBlob: (path) =>
|
||||
fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),
|
||||
|
||||
// Dashboards
|
||||
getDashboards: (envId, options = {}) => {
|
||||
const params = new URLSearchParams({ env_id: envId });
|
||||
if (options.search) params.append('search', options.search);
|
||||
|
||||
@@ -169,6 +169,23 @@
|
||||
return byName?.id || rawEnvIdOrName;
|
||||
}
|
||||
|
||||
function normalizeSupersetBaseUrl(rawUrl) {
|
||||
const baseUrl = String(rawUrl || "").trim().replace(/\/+$/, "");
|
||||
if (!baseUrl) return null;
|
||||
if (baseUrl.endsWith("/api/v1")) {
|
||||
return baseUrl.slice(0, -"/api/v1".length);
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
function resolveSupersetDashboardUrl(envId, dashboardId) {
|
||||
if (!envId || !dashboardId) return null;
|
||||
const env = environmentOptions.find((item) => item.id === envId);
|
||||
const baseUrl = normalizeSupersetBaseUrl(env?.url);
|
||||
if (!baseUrl) return null;
|
||||
return `${baseUrl}/superset/dashboard/${encodeURIComponent(String(dashboardId))}/`;
|
||||
}
|
||||
|
||||
async function loadActiveTaskDetails() {
|
||||
const taskId = normalizeTaskId(activeTaskId);
|
||||
if (!taskId) return;
|
||||
@@ -295,7 +312,17 @@
|
||||
);
|
||||
return;
|
||||
}
|
||||
const href = `/dashboards/${encodeURIComponent(String(taskSummary.primaryDashboardId))}?env_id=${encodeURIComponent(String(taskSummary.targetEnvId))}`;
|
||||
const href = resolveSupersetDashboardUrl(
|
||||
taskSummary.targetEnvId,
|
||||
taskSummary.primaryDashboardId,
|
||||
);
|
||||
if (!href) {
|
||||
addToast(
|
||||
$t.tasks?.summary_link_unavailable || "Deep link unavailable",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.open(href, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* @TEST_FIXTURE init_state -> {"taskId": "task-1"}
|
||||
* @TEST_INVARIANT correct_fetch -> verifies: [init_state]
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import { api } from "$lib/api.js";
|
||||
@@ -41,6 +41,9 @@
|
||||
let logs = [];
|
||||
let isLoading = true;
|
||||
let error = null;
|
||||
let screenshotBlobUrls = {};
|
||||
let screenshotLoadErrors = {};
|
||||
let screenshotLoadToken = 0;
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return "-";
|
||||
@@ -102,10 +105,57 @@
|
||||
openDrawerForTask(taskId);
|
||||
}
|
||||
|
||||
function cleanupScreenshotBlobUrls() {
|
||||
Object.values(screenshotBlobUrls).forEach((url) => {
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
});
|
||||
screenshotBlobUrls = {};
|
||||
}
|
||||
|
||||
async function loadScreenshotBlobUrls(paths) {
|
||||
const currentToken = ++screenshotLoadToken;
|
||||
cleanupScreenshotBlobUrls();
|
||||
screenshotLoadErrors = {};
|
||||
|
||||
if (!Array.isArray(paths) || paths.length === 0) return;
|
||||
|
||||
const nextUrls = {};
|
||||
const nextErrors = {};
|
||||
|
||||
await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
try {
|
||||
const blob = await api.getStorageFileBlob(path);
|
||||
nextUrls[path] = URL.createObjectURL(blob);
|
||||
} catch (err) {
|
||||
nextErrors[path] = err?.message || "Failed to load screenshot";
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (currentToken !== screenshotLoadToken) {
|
||||
Object.values(nextUrls).forEach((url) => URL.revokeObjectURL(url));
|
||||
return;
|
||||
}
|
||||
|
||||
screenshotBlobUrls = nextUrls;
|
||||
screenshotLoadErrors = nextErrors;
|
||||
}
|
||||
|
||||
function openScreenshot(path) {
|
||||
const url = screenshotBlobUrls[path];
|
||||
if (!url) return;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadReport();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
cleanupScreenshotBlobUrls();
|
||||
});
|
||||
|
||||
$: result = task?.result || {};
|
||||
$: checkResult = getDashboardCheckResult(result);
|
||||
$: timings = result?.timings || {};
|
||||
@@ -114,6 +164,7 @@
|
||||
: result?.screenshot_path
|
||||
? [result.screenshot_path]
|
||||
: [];
|
||||
$: void loadScreenshotBlobUrls(screenshotPaths);
|
||||
$: sentLogs = Array.isArray(result?.logs_sent_to_llm)
|
||||
? result.logs_sent_to_llm
|
||||
: [];
|
||||
@@ -256,19 +307,27 @@
|
||||
{:else}
|
||||
<div class="mt-3 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{#each screenshotPaths as path}
|
||||
<a
|
||||
href={`/api/storage/file?path=${encodeURIComponent(path)}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="block"
|
||||
<button
|
||||
type="button"
|
||||
class="block w-full text-left"
|
||||
on:click={() => openScreenshot(path)}
|
||||
disabled={!screenshotBlobUrls[path]}
|
||||
>
|
||||
<img
|
||||
src={`/api/storage/file?path=${encodeURIComponent(path)}`}
|
||||
alt="Validation screenshot"
|
||||
class="h-64 w-full rounded-lg border border-slate-200 object-cover"
|
||||
/>
|
||||
{#if screenshotBlobUrls[path]}
|
||||
<img
|
||||
src={screenshotBlobUrls[path]}
|
||||
alt="Validation screenshot"
|
||||
class="h-64 w-full rounded-lg border border-slate-200 object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-64 w-full items-center justify-center rounded-lg border border-rose-200 bg-rose-50 px-4 text-sm text-rose-700"
|
||||
>
|
||||
{screenshotLoadErrors[path] || "Failed to load screenshot"}
|
||||
</div>
|
||||
{/if}
|
||||
<p class="mt-1 truncate text-xs text-slate-500">{path}</p>
|
||||
</a>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user