-
Notifications
You must be signed in to change notification settings - Fork 0
fix: improve snapshot compare and saved jobs handling (#71, #74, #75, #82) #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import type { ExportSnapshot, SavedExportFormat, ScheduledExport } from '../../c | |
| import type { SfApi } from '../api/sf'; | ||
| import { Icon } from '../components/Icon'; | ||
| import { exportRecords, ensureCorrectExtension } from '../utils/export'; | ||
| import { flattenRecord, deriveColumns } from '../utils/records'; | ||
| import { forecastSnapshotStorage, formatStorageSize } from '../utils/scheduleForecast'; | ||
| import { diffBaselineRecords, selectComparisonKey } from '../utils/localDataDiff'; | ||
|
|
||
|
|
@@ -71,21 +72,26 @@ export function SnapshotCenterScreen(props: { | |
| } | ||
|
|
||
| async function compareWithLive(): Promise<void> { | ||
| if (!left || !props.tabId) return; | ||
| if (!left) return; | ||
| const schedule = scheduleById.get(left.scheduleId); | ||
| if (!schedule) return; | ||
| const targetOrgId = left.orgId ?? schedule.orgId; | ||
| if (!targetOrgId) { | ||
| setMessage('Cannot compare: snapshot has no org ID.'); | ||
| return; | ||
| } | ||
| setMessage('Loading live org records…'); | ||
| try { | ||
| const records: Record<string, unknown>[] = []; | ||
| let page = await props.sf.runQuery(schedule.soql, props.tabId); | ||
| let page = await props.sf.crossOrgQuery(targetOrgId, schedule.soql); | ||
| records.push(...(page.records ?? [])); | ||
| while (page.nextRecordsUrl && records.length < 100_000) { | ||
| page = await props.sf.queryMore(page.nextRecordsUrl, props.tabId); | ||
| page = await props.sf.queryMore(page.nextRecordsUrl); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the live query returns Useful? React with 👍 / 👎. |
||
| records.push(...(page.records ?? [])); | ||
| } | ||
| setLiveRecords(records); | ||
| setRightId(''); | ||
| setMessage(`Loaded ${records.length.toLocaleString()} live records.`); | ||
| setMessage(`Loaded ${records.length.toLocaleString()} live records from org ${targetOrgId}.`); | ||
| } catch (error) { | ||
| setMessage(error instanceof Error ? error.message : 'Live comparison failed.'); | ||
| } | ||
|
|
@@ -133,7 +139,11 @@ export function SnapshotCenterScreen(props: { | |
| <button class="wl-buttonText" onClick={() => persistSnapshots({ ...snapshots, [snapshot.id]: { ...snapshot, pinned: !snapshot.pinned } })}>{snapshot.pinned ? 'Unpin' : 'Pin'}</button> | ||
| <button class="wl-buttonNeutral" onClick={() => { setLeftId(snapshot.id); setRightId(''); setLiveRecords(null); }}>Compare</button> | ||
| <select class="wl-select" aria-label={`Download format for ${schedule?.name ?? snapshot.id}`} value={format} onChange={event => setFormats(current => ({ ...current, [snapshot.id]: (event.currentTarget as HTMLSelectElement).value as SavedExportFormat }))}><option value="csv">CSV</option><option value="json">JSON</option><option value="excel">Excel</option><option value="xml">XML</option></select> | ||
| <button class="wl-buttonBrand" disabled={Boolean(snapshot.error)} onClick={() => exportRecords(snapshot.records, snapshot.columns, { format, filename: ensureCorrectExtension(`${schedule?.name ?? 'snapshot'}-${snapshot.capturedAt}`, format) })}>Download</button> | ||
| <button class="wl-buttonBrand" disabled={Boolean(snapshot.error)} onClick={() => { | ||
| const flat = snapshot.records.map(record => flattenRecord(record)); | ||
| const columns = deriveColumns(flat); | ||
| return exportRecords(flat, columns, { format, filename: ensureCorrectExtension(`${schedule?.name ?? 'snapshot'}-${snapshot.capturedAt}`, format) }); | ||
| }}>Download</button> | ||
| </div></div><div class="wl-cardSection"><div class="wl-muted">{new Date(snapshot.capturedAt).toLocaleString()} · org {snapshot.orgId ?? schedule?.orgId ?? 'unknown'}{snapshot.error ? ` · ${snapshot.error}` : ''}</div></div></article>; | ||
| })}</div> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,10 +51,12 @@ export function mergeLegacyJobs( | |
| exports: ExportTemplate[], | ||
| imports: ImportTemplate[], | ||
| schedules: ScheduledExport[], | ||
| deletedLegacyIds: string[] = [], | ||
| ): SavedJob[] { | ||
| const byId = new Map(existing.map(job => [job.id, job])); | ||
| const tombstones = new Set(deletedLegacyIds); | ||
| const byId = new Map(existing.filter(job => !tombstones.has(job.id)).map(job => [job.id, job])); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After a migrated job is deleted, its ID remains tombstoned. If the user later imports a portable backup containing that same ID, Useful? React with 👍 / 👎. |
||
| for (const job of [...exports.map(jobFromExportTemplate), ...imports.map(jobFromImportTemplate), ...schedules.map(jobFromSchedule)]) { | ||
| if (!byId.has(job.id)) byId.set(job.id, job); | ||
| if (!tombstones.has(job.id) && !byId.has(job.id)) byId.set(job.id, job); | ||
| } | ||
| return Array.from(byId.values()); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When retention has pruned an older run's snapshot, this unconditionally assigns that run the nearest surviving snapshot from the same schedule, regardless of how far apart their timestamps are. Consequently every historical run appears to have an “Open result” even when its result was deleted, and the link leads users to an unrelated run's snapshots. Match the run's capture timestamp (the scheduler records
capturedAtfrom the samestartedAtvalue) or retain a suitably strict maximum delta.Useful? React with 👍 / 👎.