mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
perf: a deploy history comes a page at a time, so the diff drawer opens at once
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b6da6410e1
commit
cd6e06c638
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,7 +27,9 @@
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -37,5 +39,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "605c575a82693bd5351cc748b31c6c44916c503837cb6d189147e184f7f41caf"
|
||||
"hash": "044310c0e7745867a24a9d3653ff29c87ce48ffd89fac070c8314556f884ce55"
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version\n LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version\n WHERE flow_version.path = $1 AND flow_version.workspace_id = $2\n ORDER BY flow_version.created_at DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "deployment_msg",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7ff60d155510303e307c47dcb6153ccaac72871eb68822d8e296c0d747e55f35"
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "deployment_msg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c"
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version \n LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version\n WHERE flow_version.path = $1 AND flow_version.workspace_id = $2 \n ORDER BY flow_version.created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "deployment_msg",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a0f1c0df6bc2f1fbca50edee90e42c94445536e201b322eda6f7a90bdf38f36a"
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg,\n av.created_by as created_by, av.created_at as created_at\n FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version\n WHERE a.workspace_id = $1 AND a.path = $2\n ORDER BY array_position(a.versions, av.id) DESC NULLS LAST, av.id DESC",
|
||||
"query": "SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg,\n av.created_by as created_by, av.created_at as created_at\n FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version\n WHERE a.workspace_id = $1 AND a.path = $2\n ORDER BY array_position(a.versions, av.id) DESC NULLS LAST, av.id DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -32,7 +32,9 @@
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
@@ -43,5 +45,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c8b369881add5574c1762431e2fb58eb04892babb4684be314f369227424e640"
|
||||
"hash": "e7c8ccc5f1955cae39856baacafb7ce6482125e720fb8f53e3e9e749a69e08bd"
|
||||
}
|
||||
@@ -51,7 +51,10 @@ use windmill_common::{
|
||||
jobs::JobPayload,
|
||||
schedule::Schedule,
|
||||
triggers::MovedNativeTrigger,
|
||||
utils::{http_get_from_hub, not_found_if_none, paginate, Pagination, RunnableKind, StripPath},
|
||||
utils::{
|
||||
http_get_from_hub, not_found_if_none, paginate, paginate_with_default, Pagination,
|
||||
RunnableKind, StripPath, HISTORY_PER_PAGE,
|
||||
},
|
||||
};
|
||||
use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap;
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
@@ -900,19 +903,24 @@ async fn get_flow_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<FlowVersion>> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("flows:read:{}", path))?;
|
||||
let (per_page, offset) = paginate_with_default(pagination, HISTORY_PER_PAGE);
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let flows = sqlx::query_as!(
|
||||
FlowVersion,
|
||||
"SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version
|
||||
"SELECT flow_version.id, flow_version.created_at, flow_version.created_by, deployment_metadata.deployment_msg FROM flow_version
|
||||
LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version
|
||||
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
|
||||
ORDER BY flow_version.created_at DESC",
|
||||
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
|
||||
ORDER BY flow_version.created_at DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
path,
|
||||
w_id
|
||||
w_id,
|
||||
per_page as i64,
|
||||
offset as i64,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -73,7 +73,10 @@ use windmill_common::{
|
||||
},
|
||||
triggers::MovedNativeTrigger,
|
||||
users::username_to_permissioned_as,
|
||||
utils::{not_found_if_none, query_elems_from_hub, require_admin, Pagination, StripPath},
|
||||
utils::{
|
||||
not_found_if_none, paginate_with_default, query_elems_from_hub, require_admin, Pagination,
|
||||
StripPath, HISTORY_PER_PAGE,
|
||||
},
|
||||
worker::to_raw_value,
|
||||
HUB_BASE_URL,
|
||||
};
|
||||
@@ -3088,17 +3091,22 @@ async fn get_script_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<ScriptHistory>> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("scripts:read:{}", path))?;
|
||||
let (per_page, offset) = paginate_with_default(pagination, HISTORY_PER_PAGE);
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let query_result = sqlx::query!(
|
||||
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at, s.created_by as created_by
|
||||
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
|
||||
WHERE s.workspace_id = $1 AND s.path = $2
|
||||
ORDER by s.created_at DESC",
|
||||
ORDER by s.created_at DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
path,
|
||||
per_page as i64,
|
||||
offset as i64,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -10356,6 +10356,8 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: script history
|
||||
@@ -11761,6 +11763,8 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
tags:
|
||||
- flow
|
||||
responses:
|
||||
@@ -13295,6 +13299,8 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: app history
|
||||
|
||||
@@ -69,8 +69,9 @@ use windmill_common::{
|
||||
user_drafts::{overlay_or_draft_only, DraftUserRef, UserDraftItemKind, WithDraftOverlay},
|
||||
users::username_to_permissioned_as,
|
||||
utils::{
|
||||
http_get_from_hub, not_found_if_none, paginate, query_elems_from_hub, require_admin,
|
||||
strip_json_nul, Pagination, RunnableKind, StripPath,
|
||||
http_get_from_hub, not_found_if_none, paginate, paginate_with_default,
|
||||
query_elems_from_hub, require_admin, strip_json_nul, Pagination, RunnableKind, StripPath,
|
||||
HISTORY_PER_PAGE,
|
||||
},
|
||||
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
@@ -1237,9 +1238,11 @@ async fn get_app_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<AppHistory>> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("apps:read:{}", &path))?;
|
||||
let (per_page, offset) = paginate_with_default(pagination, HISTORY_PER_PAGE);
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
// Newest first in the order the versions were deployed, which the picker numbers
|
||||
// (`v1`, `v2`, …) and reads the head off. That is the position in `app.versions`,
|
||||
@@ -1252,9 +1255,12 @@ async fn get_app_history(
|
||||
av.created_by as created_by, av.created_at as created_at
|
||||
FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version
|
||||
WHERE a.workspace_id = $1 AND a.path = $2
|
||||
ORDER BY array_position(a.versions, av.id) DESC NULLS LAST, av.id DESC",
|
||||
ORDER BY array_position(a.versions, av.id) DESC NULLS LAST, av.id DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
path,
|
||||
per_page as i64,
|
||||
offset as i64,
|
||||
).fetch_all(&mut *tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
|
||||
@@ -479,6 +479,21 @@ pub fn paginate(pagination: Pagination) -> (usize, usize) {
|
||||
(per_page, offset)
|
||||
}
|
||||
|
||||
/// What one page of a deploy history holds when the caller asks for no size. The
|
||||
/// picker that reads these shows a screenful and fetches the next page on demand;
|
||||
/// a path a pipeline deploys carries far more versions than anyone scrolls.
|
||||
pub const HISTORY_PER_PAGE: usize = 20;
|
||||
|
||||
/// [`paginate`] for a listing whose unasked-for page should be smaller than the generic
|
||||
/// default — a version history behind a picker, where a path deployed by CI carries
|
||||
/// thousands of entries and only the first screenful is ever read.
|
||||
pub fn paginate_with_default(pagination: Pagination, default_per_page: usize) -> (usize, usize) {
|
||||
paginate(Pagination {
|
||||
per_page: Some(pagination.per_page.unwrap_or(default_per_page)),
|
||||
..pagination
|
||||
})
|
||||
}
|
||||
|
||||
pub fn paginate_without_limits(pagination: Pagination) -> (usize, usize) {
|
||||
let per_page = pagination.per_page.unwrap_or(MAX_PER_PAGE);
|
||||
let offset = (pagination.page.unwrap_or(1).max(1) - 1) * per_page;
|
||||
|
||||
@@ -142,6 +142,42 @@
|
||||
* generation is stale — a slower earlier pick, or one outlived by a drawer reset —
|
||||
* and neither replaces the diff nor clears the spinner, which `disabled` rides on. */
|
||||
let versionLoadGeneration = 0
|
||||
/** Pages of `versions` fetched after the first. Kept beside `data` so a diff swapped in
|
||||
* by a newer opening drops them along with the list they extended. */
|
||||
let extraVersions: DiffVersionOption[] = $state([])
|
||||
let moreLoader: (() => Promise<DiffVersionOption[] | undefined>) | undefined = $state(undefined)
|
||||
let loadingMore = $state(false)
|
||||
/** What the picker offers: the page the editor handed over plus whatever the reader has
|
||||
* asked for since. Every read of the version list goes through this. */
|
||||
const pickerVersions = $derived.by(() =>
|
||||
data?.mode === 'normal' ? [...(data.versions ?? []), ...extraVersions] : []
|
||||
)
|
||||
|
||||
/** The editors hand over one page, so a path a pipeline has deployed thousands of times
|
||||
* does not hold the drawer shut while its whole history arrives. */
|
||||
async function fetchMoreVersions() {
|
||||
if (!moreLoader || loadingMore) return
|
||||
loadingMore = true
|
||||
const generation = versionLoadGeneration
|
||||
try {
|
||||
const more = await moreLoader()
|
||||
// A diff swapped in while this ran owns the picker now; appending would splice
|
||||
// one item's history onto another's.
|
||||
if (generation !== versionLoadGeneration) return
|
||||
if (more?.length) {
|
||||
extraVersions = [...extraVersions, ...more]
|
||||
} else {
|
||||
// Nothing came back, so there is nothing further to ask for.
|
||||
moreLoader = undefined
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (generation === versionLoadGeneration) {
|
||||
sendUserToast(`Could not load older versions: ${e?.body ?? e?.message ?? e}`, true)
|
||||
}
|
||||
} finally {
|
||||
loadingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectVersion(id: string | undefined) {
|
||||
if (!id || !versionLoader || !data || data.mode !== 'normal') return
|
||||
@@ -159,7 +195,7 @@
|
||||
if (!value) sendUserToast(`Could not load version ${id}`, true)
|
||||
return
|
||||
}
|
||||
const opt = data.versions?.find((v) => v.id === id)
|
||||
const opt = pickerVersions.find((v) => v.id === id)
|
||||
data = {
|
||||
...data,
|
||||
deployed: prepareDiff(value),
|
||||
@@ -181,7 +217,7 @@
|
||||
* it, so "take latest" means the version the reader is looking at. */
|
||||
const headShown = $derived.by(() =>
|
||||
data?.mode === 'normal'
|
||||
? (data.versions?.find((v) => v.isHead)?.id ?? data.deployedHead)
|
||||
? (pickerVersions.find((v) => v.isHead)?.id ?? data.deployedHead)
|
||||
: undefined
|
||||
)
|
||||
/** Behind as the drawer can see it. Unknown counts as not behind: offering to adopt a
|
||||
@@ -218,6 +254,7 @@
|
||||
deployedLabel?: string
|
||||
versions?: DiffVersionOption[]
|
||||
loadVersion?: (id: string) => Promise<Value | undefined>
|
||||
loadMoreVersions?: () => Promise<DiffVersionOption[] | undefined>
|
||||
onTakeLatest?: (head?: string) => void | Promise<void>
|
||||
draftBase?: string
|
||||
deployedHead?: string
|
||||
@@ -245,6 +282,7 @@
|
||||
deployedLabel,
|
||||
versions,
|
||||
loadVersion,
|
||||
loadMoreVersions,
|
||||
onTakeLatest,
|
||||
draftBase,
|
||||
deployedHead,
|
||||
@@ -253,11 +291,14 @@
|
||||
button
|
||||
} = diff
|
||||
versionLoader = loadVersion
|
||||
moreLoader = loadMoreVersions
|
||||
headLabel = deployedLabel
|
||||
headDeployed = !deployed.draft_only ? prepareDiff(deployed) : undefined
|
||||
// A load still in flight belongs to the diff being replaced.
|
||||
versionLoadGeneration++
|
||||
loadingVersion = false
|
||||
extraVersions = []
|
||||
loadingMore = false
|
||||
selectedVersion = versions?.find((v) => v.isHead)?.id
|
||||
data = {
|
||||
mode: 'normal',
|
||||
@@ -297,12 +338,12 @@
|
||||
<!-- Outside the `contentType` check on purpose: with no differences against
|
||||
the head, picking an older version is exactly how the reader finds one,
|
||||
so the picker has to outlive the "no changes" state. -->
|
||||
{#if data.mode === 'normal' && (data.deployedLabel || data.versions?.length)}
|
||||
{#if data.mode === 'normal' && (data.deployedLabel || pickerVersions.length)}
|
||||
<div class="flex gap-2 items-center text-xs text-secondary">
|
||||
{#if data.versions?.length && versionLoader}
|
||||
{#if pickerVersions.length && versionLoader}
|
||||
<div class="w-72">
|
||||
<Select
|
||||
items={data.versions.map((v) => ({
|
||||
items={pickerVersions.map((v) => ({
|
||||
value: v.id,
|
||||
label: v.label,
|
||||
subtitle: v.subtitle
|
||||
@@ -315,6 +356,17 @@
|
||||
{#if loadingVersion}
|
||||
<Loader2 size={12} class="animate-spin shrink-0" />
|
||||
{/if}
|
||||
{#if moreLoader}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="2xs"
|
||||
disabled={loadingMore}
|
||||
on:click={fetchMoreVersions}>Load older</Button
|
||||
>
|
||||
{/if}
|
||||
{#if loadingMore}
|
||||
<Loader2 size={12} class="animate-spin shrink-0" />
|
||||
{/if}
|
||||
{:else if data.deployedLabel}
|
||||
<span class="font-medium text-primary">{data.deployedLabel}</span>
|
||||
{/if}
|
||||
|
||||
@@ -1137,17 +1137,18 @@
|
||||
|
||||
/** Deployed versions for the diff picker, newest first. Best-effort: losing the
|
||||
* list costs the picker, not the diff. */
|
||||
async function deployedVersionOptions() {
|
||||
async function deployedVersionOptions(page = 1) {
|
||||
const path = userDraftPath || initialPath
|
||||
if (!opWorkspace || !path) return undefined
|
||||
try {
|
||||
const history = await FlowService.getFlowHistory({ workspace: opWorkspace, path })
|
||||
const total = history.length
|
||||
const history = await FlowService.getFlowHistory({ workspace: opWorkspace, path, page })
|
||||
// Head is the version the payload beside this list came from, not whatever the
|
||||
// history now leads with: a deploy landing between the two fetches would
|
||||
// otherwise label the shown (older) value as the latest.
|
||||
const head = deployedVersionShown ?? history[0]?.id
|
||||
return history.map((h, i) => {
|
||||
// No ordinal: the list arrives a page at a time, so a number counted within one
|
||||
// would rename versions as more load.
|
||||
return history.map((h) => {
|
||||
const detail = [
|
||||
h.created_by,
|
||||
h.created_at ? new Date(h.created_at).toLocaleString() : undefined,
|
||||
@@ -1156,7 +1157,7 @@
|
||||
const isHead = h.id === head
|
||||
return {
|
||||
id: String(h.id),
|
||||
label: `v${total - i} · ${h.id}${isHead ? ' · latest' : ''}`,
|
||||
label: `${h.id}${isHead ? ' · latest' : ''}`,
|
||||
subtitle: detail.length ? detail.join(' · ') : undefined,
|
||||
isHead
|
||||
}
|
||||
@@ -1166,6 +1167,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Hands the drawer the next page each time the reader asks for one. Held here rather
|
||||
* than in the drawer because the page number belongs to this item's history. */
|
||||
function moreVersionsLoader() {
|
||||
let page = 1
|
||||
return async () => deployedVersionOptions(++page)
|
||||
}
|
||||
|
||||
/** The opening this editor claimed last. A path change remounts this editor while the
|
||||
* drawer stays mounted, so its teardown hands that opening back rather than leaving
|
||||
* the drawer on the item the user left. */
|
||||
@@ -1196,6 +1204,7 @@
|
||||
deployed: deployedValue ?? savedFlow,
|
||||
deployedLabel,
|
||||
versions,
|
||||
loadMoreVersions: moreVersionsLoader(),
|
||||
onTakeLatest,
|
||||
draftBase: draftBaseVersion,
|
||||
deployedHead: deployedVersionShown != null ? String(deployedVersionShown) : undefined,
|
||||
|
||||
@@ -843,18 +843,18 @@
|
||||
/** Deployed versions to offer in the diff picker, newest first. Best-effort: a
|
||||
* failure here costs the picker, not the diff, so the drawer still opens on the
|
||||
* head. `deployment_msg` is all the history endpoint carries besides the hash. */
|
||||
async function deployedVersionOptions(headHash: string | undefined) {
|
||||
async function deployedVersionOptions(headHash: string | undefined, page = 1) {
|
||||
if (!opWorkspace || !userDraftPath) return undefined
|
||||
try {
|
||||
const history = await ScriptService.getScriptHistoryByPath({
|
||||
workspace: opWorkspace,
|
||||
path: userDraftPath
|
||||
path: userDraftPath,
|
||||
page
|
||||
})
|
||||
// Numbered newest-first from the history order: the version number and hash
|
||||
// identify it, and who deployed it drops to the subtitle so the number reads
|
||||
// first. The hash stays because it is what the API and the CLI speak.
|
||||
const total = history.length
|
||||
return history.map((h, i) => {
|
||||
// The hash identifies the version — it is what the API and the CLI speak — and
|
||||
// who deployed it drops to the subtitle. No ordinal: the list arrives a page at
|
||||
// a time, so a number counted within one would rename versions as more load.
|
||||
return history.map((h) => {
|
||||
const isHead = h.script_hash === headHash
|
||||
const detail = [
|
||||
h.created_by,
|
||||
@@ -863,7 +863,7 @@
|
||||
].filter(Boolean)
|
||||
return {
|
||||
id: h.script_hash,
|
||||
label: `v${total - i} · ${h.script_hash.slice(0, 8)}${isHead ? ' · latest' : ''}`,
|
||||
label: `${h.script_hash.slice(0, 8)}${isHead ? ' · latest' : ''}`,
|
||||
subtitle: detail.length ? detail.join(' · ') : undefined,
|
||||
isHead
|
||||
}
|
||||
@@ -873,6 +873,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Hands the drawer the next page each time the reader asks for one. Held here rather
|
||||
* than in the drawer because the page number belongs to this item's history. */
|
||||
function moreVersionsLoader(headHash: string | undefined) {
|
||||
let page = 1
|
||||
return async () => deployedVersionOptions(headHash, ++page)
|
||||
}
|
||||
|
||||
/** The opening this editor claimed last. A path change remounts this editor while the
|
||||
* drawer stays mounted, so its teardown hands that opening back rather than leaving
|
||||
* the drawer on the item the user left. */
|
||||
@@ -920,6 +927,7 @@
|
||||
deployed,
|
||||
deployedLabel: deployedVersionLabel(deployed),
|
||||
versions,
|
||||
loadMoreVersions: moreVersionsLoader(headHash),
|
||||
onTakeLatest,
|
||||
draftBase: draftBaseHash,
|
||||
deployedHead: headHash,
|
||||
|
||||
@@ -27,6 +27,10 @@ export type DiffDrawerDiff =
|
||||
/** Loads one version's payload. Returning `undefined` leaves the current
|
||||
* comparison in place rather than blanking the diff. */
|
||||
loadVersion?: (id: string) => Promise<Value | undefined>
|
||||
/** Fetches the next, older page of `versions`. `versions` holds one page so the
|
||||
* drawer opens without waiting on a path a pipeline has deployed thousands of
|
||||
* times; the reader asks for the rest. Returning nothing ends the list. */
|
||||
loadMoreVersions?: () => Promise<DiffVersionOption[] | undefined>
|
||||
/** Moves the draft's base to the head and keeps its content, rendered as a
|
||||
* header action so the user takes the latest with the diff in front of them.
|
||||
* Called with the version the drawer is showing as head (from `versions`), so
|
||||
|
||||
@@ -432,17 +432,19 @@
|
||||
|
||||
/** Deployed versions for the diff picker, newest first. Best-effort: losing the
|
||||
* list costs the picker, not the diff. */
|
||||
async function deployedVersionOptions() {
|
||||
async function deployedVersionOptions(page = 1) {
|
||||
if (!opWorkspace || !appPath) return undefined
|
||||
try {
|
||||
const history = await AppService.getAppHistoryByPath({
|
||||
workspace: opWorkspace,
|
||||
path: appPath
|
||||
path: appPath,
|
||||
page
|
||||
})
|
||||
const total = history.length
|
||||
// Head is the version the payload beside this list came from; see FlowBuilder.
|
||||
const head = deployedVersionShown ?? history[0]?.version
|
||||
return history.map((h, i) => {
|
||||
// No ordinal: the list arrives a page at a time, so a number counted within one
|
||||
// would rename versions as more load.
|
||||
return history.map((h) => {
|
||||
const detail = [
|
||||
h.created_by,
|
||||
h.created_at ? new Date(h.created_at).toLocaleString() : undefined,
|
||||
@@ -451,7 +453,7 @@
|
||||
const isHead = h.version === head
|
||||
return {
|
||||
id: String(h.version),
|
||||
label: `v${total - i} · ${h.version}${isHead ? ' · latest' : ''}`,
|
||||
label: `${h.version}${isHead ? ' · latest' : ''}`,
|
||||
subtitle: detail.length ? detail.join(' · ') : undefined,
|
||||
isHead
|
||||
}
|
||||
@@ -461,6 +463,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Hands the drawer the next page each time the reader asks for one. Held here rather
|
||||
* than in the drawer because the page number belongs to this item's history. */
|
||||
function moreVersionsLoader() {
|
||||
let page = 1
|
||||
return async () => deployedVersionOptions(++page)
|
||||
}
|
||||
|
||||
/** The opening this editor claimed last. A path change remounts this editor while the
|
||||
* drawer stays mounted, so its teardown hands that opening back rather than leaving
|
||||
* the drawer on the item the user left. */
|
||||
@@ -493,6 +502,7 @@
|
||||
mode: 'normal',
|
||||
deployed: deployedValue ?? stripRawAppDiffNoise(savedApp),
|
||||
versions,
|
||||
loadMoreVersions: moreVersionsLoader(),
|
||||
onTakeLatest,
|
||||
draftBase: draftBaseVersion,
|
||||
deployedHead: deployedVersionShown != null ? String(deployedVersionShown) : undefined,
|
||||
|
||||
Reference in New Issue
Block a user