Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion gateway/sds_gateway/static/css/visualizations/waterfall.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@
margin-right: 0;
}

.waterfall-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
backdrop-filter: blur(2px);
}

#waterfallOverlayCanvas {
position: absolute;
top: 0;
Expand All @@ -65,7 +80,7 @@
/* Button states */
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
cursor: default;
}

/* Loading states */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Waterfall API Client
* Handles all API requests for waterfall visualization
*/

class WaterfallAPIClient {
constructor(captureUuid) {
this.captureUuid = captureUuid;
}

/**
* Get CSRF token from form input
*/
getCSRFToken() {
const token = document.querySelector("[name=csrfmiddlewaretoken]");
return token ? token.value : "";
}

/**
* Get create waterfall endpoint URL
*/
_getCreateWaterfallEndpoint() {
return `/api/v1/visualizations/${this.captureUuid}/create_waterfall/`;
}

/**
* Get waterfall status endpoint URL
*/
_getWaterfallStatusEndpoint(jobId) {
return `/api/v1/visualizations/${this.captureUuid}/waterfall_status/?job_id=${jobId}`;
}

/**
* Get waterfall metadata endpoint URL
*/
_getWaterfallMetadataEndpoint(jobId) {
return `/api/v1/visualizations/${this.captureUuid}/waterfall_metadata/?job_id=${jobId}`;
}

/**
* Get waterfall result endpoint URL
*/
_getWaterfallResultEndpoint(jobId, startIndex = null, endIndex = null) {
let url = `/api/v1/visualizations/${this.captureUuid}/download_waterfall/?job_id=${jobId}`;
const params = new URLSearchParams();
if (startIndex !== null) {
params.append("start_index", startIndex);
}
if (endIndex !== null) {
params.append("end_index", endIndex);
}
const queryString = params.toString();
if (queryString) {
url += `&${queryString}`;
}
return url;
}

/**
* Get post-processing status for a capture
*/
async getPostProcessingStatus() {
const response = await fetch(
`/api/latest/assets/captures/${this.captureUuid}/post_processing_status/`,
);

if (!response.ok) {
throw new Error(
`Failed to get post-processing status: ${response.status}`,
);
}

return await response.json();
}

/**
* Get waterfall metadata
*/
async getWaterfallMetadata(jobId) {
const response = await fetch(this._getWaterfallMetadataEndpoint(jobId), {
headers: {
"X-CSRFToken": this.getCSRFToken(),
},
});

if (!response.ok) {
throw new Error(`Failed to load waterfall metadata: ${response.status}`);
}

return await response.json();
}

/**
* Load a range of waterfall slices
*/
async loadWaterfallRange(jobId, startIndex, endIndex) {
const response = await fetch(
this._getWaterfallResultEndpoint(jobId, startIndex, endIndex),
{
headers: {
"X-CSRFToken": this.getCSRFToken(),
},
},
);

if (!response.ok) {
throw new Error(`Failed to load waterfall range: ${response.status}`);
}

const result = await response.json();
return result.data || [];
}

/**
* Create a waterfall processing job
*/
async createWaterfallJob() {
const response = await fetch(this._getCreateWaterfallEndpoint(), {
method: "POST",
headers: {
"X-CSRFToken": this.getCSRFToken(),
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const data = await response.json();

if (!data.uuid) {
throw new Error("Waterfall job ID not found");
}

return data;
}

/**
* Get waterfall job status
*/
async getWaterfallJobStatus(jobId) {
const response = await fetch(this._getWaterfallStatusEndpoint(jobId), {
headers: {
"X-CSRFToken": this.getCSRFToken(),
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
}
}

export { WaterfallAPIClient };
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Waterfall Cache Manager
* Manages loading and caching of waterfall slice data
*/

export class WaterfallCacheManager {
constructor(totalSlices) {
this.totalSlices = totalSlices;
this.loadedSlices = new Set(); // Track which slice indices are loaded
this.loadingRanges = new Set(); // Track which ranges are currently loading
this.sliceData = new Map(); // Map of slice index -> parsed slice data
this.maxConcurrentLoads = 3;
}

/**
* Check if a specific slice is loaded
*/
isSliceLoaded(index) {
return this.loadedSlices.has(index);
}

/**
* Check if a range of slices is fully loaded
*/
isRangeLoaded(startIndex, endIndex) {
for (let i = startIndex; i < endIndex; i++) {
if (i >= this.totalSlices) break;
if (!this.loadedSlices.has(i)) {
return false;
}
}
return true;
}

/**
* Get loaded slices for a range (may include nulls for unloaded slices)
*/
getRangeSlices(startIndex, endIndex) {
const slices = [];
for (let i = startIndex; i < endIndex; i++) {
if (i >= this.totalSlices) break;
slices.push(this.sliceData.get(i) || null);
}
return slices;
}

/**
* Add loaded slices to the cache
*
* @param {number} startIndex
* @param {Array} parsedSlices - Array of parsed slice data
*/
addLoadedSlices(startIndex, slices) {
for (let i = 0; i < slices.length; i++) {
const sliceIndex = startIndex + i;
if (sliceIndex >= this.totalSlices) break;

this.sliceData.set(sliceIndex, slices[i]);
this.loadedSlices.add(sliceIndex);
}
}

/**
* Get missing ranges that need to be loaded
*/
getMissingRanges(startIndex, endIndex) {
const missingRanges = [];
let rangeStart = null;

for (let i = startIndex; i < endIndex; i++) {
if (i >= this.totalSlices) break;

if (!this.loadedSlices.has(i)) {
if (rangeStart === null) {
rangeStart = i;
}
} else {
if (rangeStart !== null) {
// End of missing range
missingRanges.push([rangeStart, i]);
rangeStart = null;
}
}
}

if (rangeStart !== null) {
missingRanges.push([rangeStart, endIndex]);
}

return missingRanges;
}

/**
* Check if an exact range is currently being loaded
*/
isRangeLoading(startIndex, endIndex) {
const rangeKey = `${startIndex}-${endIndex}`;
return this.loadingRanges.has(rangeKey);
}

/**
* Check if the given range is fully contained by any ranges that are currently being loaded
*/
isRangeContainedByLoadingRange(startIndex, endIndex) {
for (const rangeKey of this.loadingRanges) {
const [loadingStart, loadingEnd] = rangeKey.split("-").map(Number);

if (startIndex >= loadingStart && endIndex <= loadingEnd) {
return true;
}
}
return false;
}

/**
* Mark a range as loading
*/
markRangeLoading(startIndex, endIndex) {
const rangeKey = `${startIndex}-${endIndex}`;
this.loadingRanges.add(rangeKey);
}

/**
* Mark a range as finished loading
*/
markRangeLoaded(startIndex, endIndex) {
const rangeKey = `${startIndex}-${endIndex}`;
this.loadingRanges.delete(rangeKey);
}

/**
* Get the number of loaded slices
*/
getLoadedCount() {
return this.loadedSlices.size;
}

/**
* Clear all cached data
*/
clear() {
this.loadedSlices.clear();
this.loadingRanges.clear();
this.sliceData.clear();
}

/**
* Update total slices (e.g., when metadata is loaded)
*/
setTotalSlices(totalSlices) {
this.totalSlices = totalSlices;
}
}
Loading