fix: avoid unbounded ticket board queries
This commit is contained in:
@@ -218,23 +218,22 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
|
|||||||
"Tickets and Objectives should each be a single sidebar link",
|
"Tickets and Objectives should each be a single sidebar link",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
!ticketsLoad.includes("?limit=1000") &&
|
ticketsLoad.includes("?limit=1000") &&
|
||||||
ticketsLoad.includes('workspaceApiPath(workspaceId, "/tickets/query")') &&
|
!ticketsLoad.includes("/tickets/query") &&
|
||||||
ticketsLoad.includes("ticketLaneDefinitions()") &&
|
|
||||||
ticketsLoad.includes("ticketLaneQuery(lane)") &&
|
|
||||||
ticketsPage.includes('class="ticket-kanban"') &&
|
ticketsPage.includes('class="ticket-kanban"') &&
|
||||||
ticketsPage.includes('class="ticket-lane-cards"') &&
|
ticketsPage.includes('class="ticket-lane-cards"') &&
|
||||||
|
ticketsPage.includes("lane.tickets.slice(0, lane.visibleCount)") &&
|
||||||
ticketsPage.includes("handleLaneScroll") &&
|
ticketsPage.includes("handleLaneScroll") &&
|
||||||
ticketsPage.includes("Loading 30 more"),
|
ticketsPage.includes("revealNextTickets"),
|
||||||
"Tickets list should query and incrementally scroll each Kanban lane",
|
"Tickets list should fetch lightweight summaries once and incrementally reveal each Kanban lane",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
ticketPanelModel.includes('label: "Ready + Planning"') &&
|
ticketPanelModel.includes('label: "Ready + Planning"') &&
|
||||||
ticketPanelModel.includes('label: "In progress + Queued"') &&
|
ticketPanelModel.includes('label: "In progress + Queued"') &&
|
||||||
ticketPanelModel.includes('label: "Done + Closed"') &&
|
ticketPanelModel.includes('label: "Done + Closed"') &&
|
||||||
ticketPanelModel.includes("TICKET_LANE_PAGE_SIZE = 30") &&
|
ticketPanelModel.includes("TICKET_LANE_PAGE_SIZE = 30") &&
|
||||||
ticketPanelModel.includes('sort: "updated_desc"'),
|
ticketPanelModel.includes("nextTicketLaneVisibleCount"),
|
||||||
"Ticket Kanban should combine related states into independent cursor pages",
|
"Ticket Kanban should combine related states into independent 30-item display windows",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
generatedTicketApi.includes("Generated from yoi-workspace-server") &&
|
generatedTicketApi.includes("Generated from yoi-workspace-server") &&
|
||||||
|
|||||||
@@ -189,15 +189,6 @@
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.ticket-lane-load-error {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: #d66;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
.ticket-lane-load-error button {
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
.ticket-card {
|
.ticket-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
appendUniqueTicketSummaries,
|
nextTicketLaneVisibleCount,
|
||||||
TICKET_LANE_PAGE_SIZE,
|
TICKET_LANE_PAGE_SIZE,
|
||||||
ticketLaneDefinitions,
|
|
||||||
ticketLaneQuery,
|
|
||||||
ticketLanes,
|
ticketLanes,
|
||||||
ticketWorkerLaunchHref,
|
ticketWorkerLaunchHref,
|
||||||
ticketWorkerMessage,
|
ticketWorkerMessage,
|
||||||
@@ -83,47 +81,13 @@ Deno.test("ticketLanes combines workflow states and sorts by state then update t
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("ticket lane queries request independent pages of 30", () => {
|
Deno.test("ticket lane visibility advances in bounded pages of 30", () => {
|
||||||
const [readyPlanning, inprogressQueued, doneClosed] = ticketLaneDefinitions();
|
|
||||||
|
|
||||||
assertEquals(TICKET_LANE_PAGE_SIZE, 30);
|
assertEquals(TICKET_LANE_PAGE_SIZE, 30);
|
||||||
assertEquals(ticketLaneQuery(readyPlanning).states, ["ready", "planning"]);
|
assertEquals(nextTicketLaneVisibleCount(0, 95), 30);
|
||||||
assertEquals(ticketLaneQuery(inprogressQueued).states, [
|
assertEquals(nextTicketLaneVisibleCount(30, 95), 60);
|
||||||
"inprogress",
|
assertEquals(nextTicketLaneVisibleCount(60, 95), 90);
|
||||||
"queued",
|
assertEquals(nextTicketLaneVisibleCount(90, 95), 95);
|
||||||
]);
|
assertEquals(nextTicketLaneVisibleCount(95, 95), 95);
|
||||||
assertEquals(ticketLaneQuery(doneClosed, "next-page"), {
|
|
||||||
attention: [],
|
|
||||||
cursor: "next-page",
|
|
||||||
event_kinds: [],
|
|
||||||
evidence: [],
|
|
||||||
limit: 30,
|
|
||||||
linked_objective_id: null,
|
|
||||||
query: null,
|
|
||||||
related_ticket_id: null,
|
|
||||||
relation_kind: null,
|
|
||||||
review_status: null,
|
|
||||||
sort: "updated_desc",
|
|
||||||
states: ["done", "closed"],
|
|
||||||
updated_after: null,
|
|
||||||
updated_before: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test("incremental Ticket pages preserve order and discard duplicate ids", () => {
|
|
||||||
const current = [
|
|
||||||
{ id: "first", title: "First", state: "ready", priority: "1" },
|
|
||||||
{ id: "second", title: "Second", state: "planning", priority: "2" },
|
|
||||||
] as TicketSummary[];
|
|
||||||
const incoming = [
|
|
||||||
{ id: "second", title: "Duplicate", state: "planning", priority: "2" },
|
|
||||||
{ id: "third", title: "Third", state: "planning", priority: "3" },
|
|
||||||
] as TicketSummary[];
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
appendUniqueTicketSummaries(current, incoming).map((ticket) => ticket.id),
|
|
||||||
["first", "second", "third"],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("ticket worker launch uses the common Worker route and bounded Ticket context", () => {
|
Deno.test("ticket worker launch uses the common Worker route and bounded Ticket context", () => {
|
||||||
@@ -167,15 +131,12 @@ Deno.test("ticket panel starts the Orchestrator explicitly and gates orchestrati
|
|||||||
assertIncludes(panelSource, '{ method: "POST" }');
|
assertIncludes(panelSource, '{ method: "POST" }');
|
||||||
assertIncludes(panelSource, "Start Orchestrator");
|
assertIncludes(panelSource, "Start Orchestrator");
|
||||||
assertIncludes(panelSource, "orchestrator.data?.online");
|
assertIncludes(panelSource, "orchestrator.data?.online");
|
||||||
assertIncludes(
|
assertIncludes(panelSource, "lane.tickets.slice(0, lane.visibleCount)");
|
||||||
panelSource,
|
|
||||||
'workspaceApiPath(data.workspaceId, "/tickets/query")',
|
|
||||||
);
|
|
||||||
assertIncludes(
|
assertIncludes(
|
||||||
panelSource,
|
panelSource,
|
||||||
"onscroll={(event) => handleLaneScroll(event, lane.id)}",
|
"onscroll={(event) => handleLaneScroll(event, lane.id)}",
|
||||||
);
|
);
|
||||||
assertIncludes(panelSource, "Loading 30 more…");
|
assertIncludes(panelSource, "Scroll for");
|
||||||
assertIncludes(detailSource, "{#if orchestratorOnline}");
|
assertIncludes(detailSource, "{#if orchestratorOnline}");
|
||||||
assertIncludes(detailSource, "!orchestratorOnline");
|
assertIncludes(detailSource, "!orchestratorOnline");
|
||||||
assertIncludes(detailSource, "Orchestrator offline");
|
assertIncludes(detailSource, "Orchestrator offline");
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import type {
|
import type { TicketDetail, TicketSummary } from "$lib/generated/ticket-api";
|
||||||
TicketDetail,
|
|
||||||
TicketQueryItem,
|
|
||||||
TicketQueryRequest,
|
|
||||||
TicketSummary,
|
|
||||||
} from "$lib/generated/ticket-api";
|
|
||||||
|
|
||||||
export const TICKET_STATES = [
|
export const TICKET_STATES = [
|
||||||
"planning",
|
"planning",
|
||||||
@@ -77,57 +72,11 @@ export type TicketLane = {
|
|||||||
tickets: TicketCardSummary[];
|
tickets: TicketCardSummary[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ticketLaneDefinitions(): readonly TicketLaneDefinition[] {
|
export function nextTicketLaneVisibleCount(
|
||||||
return LANE_DEFINITIONS;
|
current: number,
|
||||||
}
|
total: number,
|
||||||
|
): number {
|
||||||
export function ticketLaneQuery(
|
return Math.min(total, current + TICKET_LANE_PAGE_SIZE);
|
||||||
lane: { states: readonly TicketState[] },
|
|
||||||
cursor: string | null = null,
|
|
||||||
): TicketQueryRequest {
|
|
||||||
return {
|
|
||||||
attention: [],
|
|
||||||
cursor,
|
|
||||||
event_kinds: [],
|
|
||||||
evidence: [],
|
|
||||||
limit: TICKET_LANE_PAGE_SIZE,
|
|
||||||
linked_objective_id: null,
|
|
||||||
query: null,
|
|
||||||
related_ticket_id: null,
|
|
||||||
relation_kind: null,
|
|
||||||
review_status: null,
|
|
||||||
sort: "updated_desc",
|
|
||||||
states: [...lane.states],
|
|
||||||
updated_after: null,
|
|
||||||
updated_before: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ticketSummaryFromQueryItem(
|
|
||||||
item: TicketQueryItem,
|
|
||||||
): TicketCardSummary {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
priority: item.priority === null ? "normal" : String(item.priority),
|
|
||||||
state: item.state,
|
|
||||||
title: item.title,
|
|
||||||
updated_at: item.updated_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appendUniqueTicketSummaries(
|
|
||||||
current: TicketCardSummary[],
|
|
||||||
incoming: TicketCardSummary[],
|
|
||||||
): TicketCardSummary[] {
|
|
||||||
const ids = new Set(current.map((ticket) => ticket.id));
|
|
||||||
return [
|
|
||||||
...current,
|
|
||||||
...incoming.filter((ticket) => {
|
|
||||||
if (ids.has(ticket.id)) return false;
|
|
||||||
ids.add(ticket.id);
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatedAt(ticket: TicketCardSummary): number {
|
function updatedAt(ticket: TicketCardSummary): number {
|
||||||
|
|||||||
@@ -1,40 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import type { TicketQueryResponse } from "$lib/generated/ticket-api";
|
import type { TicketListResponse } from "$lib/generated/ticket-api";
|
||||||
import type { ApiResult } from "$lib/workspace/api/http";
|
import type { ApiResult } from "$lib/workspace/api/http";
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
import {
|
import {
|
||||||
appendUniqueTicketSummaries,
|
nextTicketLaneVisibleCount,
|
||||||
ticketLaneQuery,
|
TICKET_LANE_PAGE_SIZE,
|
||||||
ticketSummaryFromQueryItem,
|
ticketLanes,
|
||||||
type TicketCardSummary,
|
type TicketLane,
|
||||||
type TicketLaneId,
|
type TicketLaneId,
|
||||||
type TicketState,
|
|
||||||
type WorkspaceOrchestratorStatus,
|
type WorkspaceOrchestratorStatus,
|
||||||
} from "$lib/workspace/tickets/ticket-panel";
|
} from "$lib/workspace/tickets/ticket-panel";
|
||||||
import "$lib/workspace/styles/tickets.css";
|
import "$lib/workspace/styles/tickets.css";
|
||||||
|
|
||||||
type LanePage = {
|
type VisibleTicketLane = TicketLane & { visibleCount: number };
|
||||||
id: TicketLaneId;
|
|
||||||
label: string;
|
|
||||||
states: TicketState[];
|
|
||||||
tickets: TicketCardSummary[];
|
|
||||||
nextCursor: string | null;
|
|
||||||
hasMore: boolean;
|
|
||||||
error: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data } = $props<{
|
const { data } = $props<{
|
||||||
data: {
|
data: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
ticketLanePages: LanePage[];
|
tickets: ApiResult<TicketListResponse>;
|
||||||
orchestrator: ApiResult<WorkspaceOrchestratorStatus>;
|
orchestrator: ApiResult<WorkspaceOrchestratorStatus>;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let lanes = $state<(LanePage & { loading: boolean })[]>(
|
let lanes = $state<VisibleTicketLane[]>(
|
||||||
untrack(() =>
|
untrack(() =>
|
||||||
data.ticketLanePages.map((lane: LanePage) => ({ ...lane, loading: false }))
|
ticketLanes(data.tickets.data?.items ?? []).map((lane) => ({
|
||||||
|
...lane,
|
||||||
|
visibleCount: Math.min(TICKET_LANE_PAGE_SIZE, lane.tickets.length),
|
||||||
|
}))
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let orchestrator = $state<ApiResult<WorkspaceOrchestratorStatus>>(
|
let orchestrator = $state<ApiResult<WorkspaceOrchestratorStatus>>(
|
||||||
@@ -42,7 +36,7 @@
|
|||||||
);
|
);
|
||||||
let orchestratorStarting = $state(false);
|
let orchestratorStarting = $state(false);
|
||||||
const displayedTicketCount = $derived(
|
const displayedTicketCount = $derived(
|
||||||
lanes.reduce((count, lane) => count + lane.tickets.length, 0),
|
lanes.reduce((count, lane) => count + lane.visibleCount, 0),
|
||||||
);
|
);
|
||||||
const LANE_LOAD_THRESHOLD_PX = 96;
|
const LANE_LOAD_THRESHOLD_PX = 96;
|
||||||
|
|
||||||
@@ -63,64 +57,18 @@
|
|||||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString();
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLane(
|
function revealNextTickets(laneId: TicketLaneId): void {
|
||||||
laneId: TicketLaneId,
|
lanes = lanes.map((lane) =>
|
||||||
update: (lane: LanePage & { loading: boolean }) =>
|
lane.id === laneId
|
||||||
LanePage & { loading: boolean },
|
? {
|
||||||
): void {
|
...lane,
|
||||||
lanes = lanes.map((lane) => lane.id === laneId ? update(lane) : lane);
|
visibleCount: nextTicketLaneVisibleCount(
|
||||||
|
lane.visibleCount,
|
||||||
|
lane.tickets.length,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
: lane
|
||||||
async function loadMoreTickets(laneId: TicketLaneId): Promise<void> {
|
|
||||||
const lane = lanes.find((candidate) => candidate.id === laneId);
|
|
||||||
if (!lane || lane.loading || (!lane.hasMore && !lane.error)) return;
|
|
||||||
|
|
||||||
updateLane(laneId, (current) => ({
|
|
||||||
...current,
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let result: ApiResult<TicketQueryResponse>;
|
|
||||||
try {
|
|
||||||
result = await loadJson<TicketQueryResponse>(
|
|
||||||
fetch,
|
|
||||||
workspaceApiPath(data.workspaceId, "/tickets/query"),
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify(ticketLaneQuery(lane, lane.nextCursor)),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
updateLane(laneId, (current) => ({
|
|
||||||
...current,
|
|
||||||
loading: false,
|
|
||||||
error: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Unable to load more Tickets.",
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.data) {
|
|
||||||
updateLane(laneId, (current) => ({
|
|
||||||
...current,
|
|
||||||
loading: false,
|
|
||||||
error: result.error ?? "Unable to load more Tickets.",
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const incoming = result.data.items.map(ticketSummaryFromQueryItem);
|
|
||||||
updateLane(laneId, (current) => ({
|
|
||||||
...current,
|
|
||||||
tickets: appendUniqueTicketSummaries(current.tickets, incoming),
|
|
||||||
nextCursor: result.data?.page.next_cursor ?? null,
|
|
||||||
hasMore: result.data?.page.has_more ?? false,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLaneScroll(event: Event, laneId: TicketLaneId): void {
|
function handleLaneScroll(event: Event, laneId: TicketLaneId): void {
|
||||||
@@ -128,7 +76,7 @@
|
|||||||
const distanceFromBottom = element.scrollHeight - element.scrollTop -
|
const distanceFromBottom = element.scrollHeight - element.scrollTop -
|
||||||
element.clientHeight;
|
element.clientHeight;
|
||||||
if (distanceFromBottom <= LANE_LOAD_THRESHOLD_PX) {
|
if (distanceFromBottom <= LANE_LOAD_THRESHOLD_PX) {
|
||||||
void loadMoreTickets(laneId);
|
revealNextTickets(laneId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -160,11 +108,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ticket-panel-summary" aria-label="Ticket summary">
|
<div class="ticket-panel-summary" aria-label="Ticket summary">
|
||||||
<strong>{displayedTicketCount}</strong>
|
<strong>{displayedTicketCount}</strong>
|
||||||
<span>tickets loaded</span>
|
<span>tickets displayed</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if data.tickets.error}
|
||||||
|
<p class="workspace-callout is-error">Tickets: {data.tickets.error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if orchestrator.error}
|
{#if orchestrator.error}
|
||||||
<p class="workspace-callout is-error">
|
<p class="workspace-callout is-error">
|
||||||
Orchestrator status: {orchestrator.error}
|
Orchestrator status: {orchestrator.error}
|
||||||
@@ -177,6 +129,8 @@
|
|||||||
|
|
||||||
<section class="ticket-kanban" aria-label="Ticket workflow board">
|
<section class="ticket-kanban" aria-label="Ticket workflow board">
|
||||||
{#each lanes as lane (lane.id)}
|
{#each lanes as lane (lane.id)}
|
||||||
|
{@const displayedTickets = lane.tickets.slice(0, lane.visibleCount)}
|
||||||
|
{@const hasMore = lane.visibleCount < lane.tickets.length}
|
||||||
<section class="ticket-lane" data-state={lane.id}>
|
<section class="ticket-lane" data-state={lane.id}>
|
||||||
<header class="ticket-lane-header">
|
<header class="ticket-lane-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -184,7 +138,7 @@
|
|||||||
<h2>{lane.label}</h2>
|
<h2>{lane.label}</h2>
|
||||||
</div>
|
</div>
|
||||||
<span class="ticket-lane-count">
|
<span class="ticket-lane-count">
|
||||||
{lane.tickets.length}{lane.hasMore ? "+" : ""}
|
{displayedTickets.length}{hasMore ? "+" : ""}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -192,7 +146,7 @@
|
|||||||
class="ticket-lane-cards"
|
class="ticket-lane-cards"
|
||||||
onscroll={(event) => handleLaneScroll(event, lane.id)}
|
onscroll={(event) => handleLaneScroll(event, lane.id)}
|
||||||
>
|
>
|
||||||
{#each lane.tickets as ticket (ticket.id)}
|
{#each displayedTickets as ticket (ticket.id)}
|
||||||
<a
|
<a
|
||||||
class="ticket-card"
|
class="ticket-card"
|
||||||
href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(ticket.id)}`}
|
href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(ticket.id)}`}
|
||||||
@@ -205,24 +159,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
{#if !lane.error}
|
|
||||||
<div class="ticket-lane-empty">No tickets</div>
|
<div class="ticket-lane-empty">No tickets</div>
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if lane.loading}
|
{#if hasMore}
|
||||||
<p class="ticket-lane-load-status" aria-live="polite">Loading 30 more…</p>
|
<p class="ticket-lane-load-status" aria-live="polite">
|
||||||
{:else if lane.error}
|
Scroll for {Math.min(TICKET_LANE_PAGE_SIZE, lane.tickets.length - lane.visibleCount)} more
|
||||||
<div class="ticket-lane-load-error">
|
</p>
|
||||||
<small>{lane.error}</small>
|
{:else if displayedTickets.length > 0}
|
||||||
<button
|
<p class="ticket-lane-load-status">All tickets displayed.</p>
|
||||||
class="workspace-secondary-button"
|
|
||||||
type="button"
|
|
||||||
onclick={() => loadMoreTickets(lane.id)}
|
|
||||||
>Retry</button>
|
|
||||||
</div>
|
|
||||||
{:else if !lane.hasMore && lane.tickets.length > 0}
|
|
||||||
<p class="ticket-lane-load-status">All tickets loaded.</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,63 +1,20 @@
|
|||||||
import type { TicketQueryResponse } from "$lib/generated/ticket-api";
|
import type { TicketListResponse } from "$lib/generated/ticket-api";
|
||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
import {
|
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
|
||||||
ticketLaneDefinitions,
|
|
||||||
ticketLaneQuery,
|
|
||||||
ticketSummaryFromQueryItem,
|
|
||||||
type WorkspaceOrchestratorStatus,
|
|
||||||
} from "$lib/workspace/tickets/ticket-panel";
|
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load = (async ({ fetch, params }) => {
|
||||||
const workspaceId = params.workspaceId;
|
const workspaceId = params.workspaceId;
|
||||||
const ticketLanePagesPromise = Promise.all(
|
const [tickets, orchestrator] = await Promise.all([
|
||||||
ticketLaneDefinitions().map(async (lane) => {
|
loadJson<TicketListResponse>(
|
||||||
try {
|
|
||||||
const page = await loadJson<TicketQueryResponse>(
|
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(workspaceId, "/tickets/query"),
|
`${workspaceApiPath(workspaceId, "/tickets")}?limit=1000`,
|
||||||
{
|
),
|
||||||
method: "POST",
|
|
||||||
headers: { "content-type": "application/json" },
|
|
||||||
body: JSON.stringify(ticketLaneQuery(lane)),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
id: lane.id,
|
|
||||||
label: lane.label,
|
|
||||||
states: [...lane.states],
|
|
||||||
tickets: page.data?.items.map(ticketSummaryFromQueryItem) ?? [],
|
|
||||||
nextCursor: page.data?.page.next_cursor ?? null,
|
|
||||||
hasMore: page.data?.page.has_more ?? false,
|
|
||||||
error: page.error,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
id: lane.id,
|
|
||||||
label: lane.label,
|
|
||||||
states: [...lane.states],
|
|
||||||
tickets: [],
|
|
||||||
nextCursor: null,
|
|
||||||
hasMore: false,
|
|
||||||
error: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Unable to load Tickets.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [ticketLanePages, orchestrator] = await Promise.all([
|
|
||||||
ticketLanePagesPromise,
|
|
||||||
loadJson<WorkspaceOrchestratorStatus>(
|
loadJson<WorkspaceOrchestratorStatus>(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(workspaceId, "/orchestrator"),
|
workspaceApiPath(workspaceId, "/orchestrator"),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return { workspaceId, tickets, orchestrator };
|
||||||
workspaceId,
|
}) satisfies PageLoad;
|
||||||
ticketLanePages,
|
|
||||||
orchestrator,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user