Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
needs: test
needs: [test, frontend-test]
permissions:
contents: read
packages: write
Expand Down Expand Up @@ -198,3 +198,20 @@ jobs:
docker run --rm \
-v "${GITHUB_WORKSPACE}:/src:ro" \
legacy-tests:${{ matrix.lancedb }}

frontend-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24'

- name: Check frontend syntax
run: node --check web/vanilla/app.js

- name: Run frontend tests
run: node --test web/vanilla/tests/*.test.js
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Frontend content is now built with safe DOM APIs instead of `innerHTML`, preventing dataset-controlled column names from injecting markup through vector tooltips (#32).
- Older dataset, metadata, and row responses no longer overwrite a newer selection when requests finish out of order (#79).
- `docker build` no longer fails with "the destination must be a directory and end with a /". `COPY backend/*.py .` needs a trailing slash when it copies more than one file. The classic builder rejected it, the BuildKit builder did not (#62).
- CI publishes no image until the test job passes. The build job now depends on the test job, so a failing test stops the release (#66).
- CI and release builds preserve the legacy LanceDB 0.3.1, 0.3.4, and 0.5 variants by reusing their last published dependency layers. Their packages are no longer available from PyPI (#92).
Expand Down
45 changes: 42 additions & 3 deletions web/vanilla/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class LanceViewer {
this.dataPathConfigured = false;
this.currentDataLocation = '';
this.currentReference = 'main';
this.datasetListRequestId = 0;
this.metadataRequestId = 0;
this.dataRequestId = 0;

this.initializeElements();
this.setupEventListeners();
Expand Down Expand Up @@ -124,6 +127,8 @@ class LanceViewer {
this.currentReference = this.elements.datasetReference.value.trim() || 'main';
this.elements.datasetReference.value = this.currentReference;
this.elements.connectionError.textContent = '';
this.metadataRequestId++;
this.dataRequestId++;
this.currentDataset = null;
this.elements.datasetHeader.style.display = 'none';
this.elements.columnSection.style.display = 'none';
Expand Down Expand Up @@ -190,6 +195,8 @@ class LanceViewer {
}

async loadDatasets() {
const requestId = ++this.datasetListRequestId;

try {
this.replaceWithMessage(this.elements.datasetList, 'Loading datasets...', 'loading');
const params = this.contextParams(false);
Expand All @@ -200,11 +207,13 @@ class LanceViewer {
}
const data = await response.json();

if (requestId !== this.datasetListRequestId) return false;

this.elements.datasetList.replaceChildren();

if (data.datasets.length === 0) {
this.replaceWithMessage(this.elements.datasetList, 'No datasets found', 'loading');
return;
return true;
}

data.datasets.forEach(dataset => {
Expand All @@ -214,9 +223,13 @@ class LanceViewer {
item.addEventListener('click', () => this.selectDataset(dataset, item));
this.elements.datasetList.appendChild(item);
});
return true;
} catch (error) {
if (requestId !== this.datasetListRequestId) return false;

this.replaceWithMessage(this.elements.datasetList, 'Failed to load datasets', 'error');
this.showConnectionError(error.message);
return false;
}
}

Expand All @@ -242,19 +255,33 @@ class LanceViewer {
}

async loadMetadata() {
const requestId = ++this.metadataRequestId;
const datasetName = this.currentDataset;

try {
const params = this.contextParams();
const response = await fetch(
`${this.apiBase}/datasets/${encodeURIComponent(this.currentDataset)}/metadata?${params}`
`${this.apiBase}/datasets/${encodeURIComponent(datasetName)}/metadata?${params}`
);
if (!response.ok) {
throw new Error(await this.responseError(response));
}
const metadata = await response.json();

if (
requestId !== this.metadataRequestId
|| datasetName !== this.currentDataset
) return false;

this.renderSchema(metadata.fields);
this.renderColumns(metadata.columns);
return true;
} catch (error) {
if (
requestId !== this.metadataRequestId
|| datasetName !== this.currentDataset
) return false;

this.showConnectionError(error.message);
this.showError(error.message);
return false;
Expand Down Expand Up @@ -328,6 +355,8 @@ class LanceViewer {
async loadData() {
if (!this.currentDataset) return;

const requestId = ++this.dataRequestId;
const datasetName = this.currentDataset;
this.showLoading();

try {
Expand All @@ -343,20 +372,30 @@ class LanceViewer {
}

const response = await fetch(
`${this.apiBase}/datasets/${encodeURIComponent(this.currentDataset)}/rows?${params}`
`${this.apiBase}/datasets/${encodeURIComponent(datasetName)}/rows?${params}`
);
if (!response.ok) {
throw new Error(await this.responseError(response));
}
const data = await response.json();

if (
requestId !== this.dataRequestId
|| datasetName !== this.currentDataset
) return false;

this.totalRows = data.total;
this.renderTable(data.rows);
this.updatePagination();
this.hideLoading();
return true;

} catch (error) {
if (
requestId !== this.dataRequestId
|| datasetName !== this.currentDataset
) return false;

this.hideLoading();
this.showConnectionError(error.message);
this.showError(error.message);
Expand Down
Loading