Files
seaweedfs/weed/admin/view/app/s3vectors_buckets.templ
T
Chris LuandClaude Opus 5.5 a074538c8f admin: manage S3 vector buckets (#12334)
* s3vectors: run an operation for trusted local tooling

Like s3tables.Manager.Execute, Server.Execute runs one S3 Vectors operation
through its handler as a given identity, without a signed request. The admin
console can then manage vector buckets through the same validation,
authorization and catalog steps as the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* admin: list, inspect, create and delete vector buckets over the API

/api/s3vectors runs the S3 Vectors operations through Server.Execute as the
admin account: list vector buckets with their encryption, read one bucket
with its policy and each index's data type, dimension, distance metric and
non-filterable keys, create and delete a bucket, and delete an index. Going
through the operations keeps the API's rules, such as ICEBERG buckets not
being vector buckets and the implicit index namespace not blocking a delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* s3vectors: export the rule for which table buckets are vector buckets

IsVectorBucketFormat names the check ListVectorBuckets and isVectorBucket
already make, so the admin console can tell the same buckets apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* admin: vector bucket pages

Object Store > Vector Buckets lists the vector buckets with their ARN,
encryption and creation time, creates and deletes them, and opens one bucket
with its indexes (data type, dimension, distance metric, non-filterable keys)
and its policy, where an index can be deleted. A LANCE row on the Table
Buckets page links to the same bucket's vector page, since both APIs serve it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* admin: stop the vector bucket and index lists at the list limit

A page cut short by filtered ICEBERG buckets let the next page carry
the list past s3TablesAdminListLimit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* admin: say when a vector bucket or index list is cut at the limit

The lists stop at s3TablesAdminListLimit while more remain, so mark
them truncated and point to an S3 Vectors client for the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* admin: indent the vector bucket policy

A policy stored as one line ran off the right edge of the details page.
Policies that are not valid JSON are shown as stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* admin: show a deeply nested vector bucket policy as stored

Indenting multiplies the size of a deeply nested document, so a policy
past 32 levels of nesting is shown without indenting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:25:24 -08:00

190 lines
7.4 KiB
Templ

package app
import (
"fmt"
"net/url"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ S3VectorsBuckets(data dash.S3VectorsBucketsData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="bi bi-bounding-box-circles me-2"></i>Vector Buckets
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createS3VectorsBucketModal">
<i class="bi bi-plus me-1"></i>Create Bucket
</button>
</div>
</div>
</div>
<div class="alert alert-info mb-4">
<i class="bi bi-info-circle me-2"></i>
Vector buckets serve the Amazon S3 Vectors API on the S3 port. Each one is a LANCE table bucket that keeps its indexes in its <code>vector</code> namespace.
Indexes are created and filled with an S3 Vectors client, which needs a running <code>weed-vector</code> engine.
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-bounding-box-circles me-2"></i>{ fmt.Sprintf("%d vector buckets", data.TotalBuckets) }
if data.Truncated {
<span class="text-muted small ms-2">{ fmt.Sprintf("showing the first %d; list the rest with an S3 Vectors client", data.TotalBuckets) }</span>
}
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" width="100%" cellspacing="0" id="s3vectorsBucketsTable">
<thead>
<tr>
<th>Name</th>
<th>ARN</th>
<th>Encryption</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, bucket := range data.Buckets {
<tr>
<td>
<a href={ dash.PUrl(ctx, "/object-store/vector-buckets/"+url.PathEscape(bucket.Name)) }>{ bucket.Name }</a>
</td>
<td class="text-muted small">{ bucket.ARN }</td>
<td>
if bucket.Encryption != "" {
<span class="badge bg-success">{ bucket.Encryption }</span>
} else {
<span class="text-muted">None</span>
}
</td>
<td>{ bucket.CreatedAt.Format("2006-01-02 15:04") }</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a class="btn btn-outline-primary btn-sm" href={ dash.PUrl(ctx, "/object-store/vector-buckets/"+url.PathEscape(bucket.Name)) } title="Indexes">
<i class="bi bi-folder2-open"></i>
</a>
<button type="button" class="btn btn-outline-danger btn-sm s3vectors-delete-bucket-btn" data-bucket-name={ bucket.Name } title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
}
if len(data.Buckets) == 0 {
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-bounding-box-circles icon-3x mb-3 text-muted"></i>
<div>
<h5>No vector buckets found</h5>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="createS3VectorsBucketModal" tabindex="-1" aria-labelledby="createS3VectorsBucketModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createS3VectorsBucketModalLabel">
<i class="bi bi-plus me-2"></i>Create Vector Bucket
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="createS3VectorsBucketForm">
<div class="modal-body">
<div class="mb-3">
<label for="s3vectorsBucketName" class="form-label">Bucket Name</label>
<input type="text" class="form-control" id="s3vectorsBucketName" name="name" placeholder="vector-bucket-name" required/>
<div class="form-text">3 to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit.</div>
</div>
<div class="form-text">
The bucket is created without encryption. To encrypt it, create it with an S3 Vectors client and <code>encryptionConfiguration</code>, which the S3 gateway checks against its SSE-S3 key.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus me-1"></i>Create
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="deleteS3VectorsBucketModal" tabindex="-1" aria-labelledby="deleteS3VectorsBucketModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteS3VectorsBucketModalLabel">
<i class="bi bi-exclamation-triangle me-2 text-warning"></i>Delete Vector Bucket
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Delete the vector bucket <strong id="deleteS3VectorsBucketName"></strong>? A bucket that still has indexes is not deleted.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="deleteS3VectorsBucketBtn">
<i class="bi bi-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const deleteModalEl = document.getElementById('deleteS3VectorsBucketModal');
const deleteModal = new bootstrap.Modal(deleteModalEl);
document.querySelectorAll('.s3vectors-delete-bucket-btn').forEach(button => {
button.addEventListener('click', function () {
deleteModalEl.dataset.bucketName = this.dataset.bucketName || '';
document.getElementById('deleteS3VectorsBucketName').textContent = this.dataset.bucketName || '';
deleteModal.show();
});
});
document.getElementById('deleteS3VectorsBucketBtn').addEventListener('click', async function () {
const bucket = deleteModalEl.dataset.bucketName;
if (!bucket) return;
try {
const response = await fetch(s3tBasePath('/api/s3vectors/buckets?bucket=' + encodeURIComponent(bucket)), { method: 'DELETE', headers: s3tWriteHeaders() });
const data = await response.json();
if (!response.ok) {
alert(data.error || 'Failed to delete vector bucket');
return;
}
location.reload();
} catch (error) {
alert('Failed to delete vector bucket: ' + error.message);
}
});
document.getElementById('createS3VectorsBucketForm').addEventListener('submit', async function (e) {
e.preventDefault();
const name = document.getElementById('s3vectorsBucketName').value.trim();
try {
const response = await fetch(s3tBasePath('/api/s3vectors/buckets'), {
method: 'POST',
headers: s3tWriteHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ name: name })
});
const data = await response.json();
if (!response.ok) {
alert(data.error || 'Failed to create vector bucket');
return;
}
location.reload();
} catch (error) {
alert('Failed to create vector bucket: ' + error.message);
}
});
});
</script>
}