mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 02:57:44 +01:00
* 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>
514 lines
21 KiB
Templ
514 lines
21 KiB
Templ
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors"
|
|
)
|
|
|
|
templ S3TablesBuckets(data dash.S3TablesBucketsData) {
|
|
<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-table me-2"></i>S3 Tables 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="#createS3TablesBucketModal">
|
|
<i class="bi bi-plus me-1"></i>Create Bucket
|
|
</button>
|
|
</div>
|
|
if data.IcebergPort > 0 {
|
|
<div class="btn-group me-2">
|
|
<a id="s3tables-iceberg-rest-link" href={ templ.SafeURL(fmt.Sprintf("//localhost:%d/v1/config", data.IcebergPort)) } target="_blank" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-snow me-1"></i>Iceberg REST API
|
|
</a>
|
|
</div>
|
|
}
|
|
if data.LancePort > 0 {
|
|
<div class="btn-group me-2">
|
|
<a id="s3tables-lance-rest-link" href={ templ.SafeURL(fmt.Sprintf("//localhost:%d/v1/table", data.LancePort)) } target="_blank" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-bounding-box me-1"></i>Lance Namespace API
|
|
</a>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div id="s3tables-buckets-content" data-iceberg-port={ fmt.Sprintf("%d", data.IcebergPort) } data-lance-port={ fmt.Sprintf("%d", data.LancePort) }>
|
|
if data.IcebergPort > 0 || data.LancePort > 0 {
|
|
<div class="alert alert-info mb-4">
|
|
<div class="d-flex align-items-start">
|
|
<i class="bi bi-info-circle icon-2x me-3"></i>
|
|
<div>
|
|
<strong>Catalog endpoints</strong>
|
|
<p class="mb-0 mt-1">
|
|
Each table bucket is a catalog. Which endpoint serves it depends on the format it holds.
|
|
</p>
|
|
<div class="mt-2">
|
|
if data.IcebergPort > 0 {
|
|
<div>
|
|
<span class="badge bg-primary">ICEBERG</span>
|
|
<code class="ms-1"><span class="s3tables-origin" data-port={ fmt.Sprintf("%d", data.IcebergPort) }>{ fmt.Sprintf("http://localhost:%d", data.IcebergPort) }</span>/v1</code>
|
|
</div>
|
|
}
|
|
if data.LancePort > 0 {
|
|
<div class="mt-1">
|
|
<span class="badge bg-success">LANCE</span>
|
|
<code class="ms-1"><span class="s3tables-origin" data-port={ fmt.Sprintf("%d", data.LancePort) }>{ fmt.Sprintf("http://localhost:%d", data.LancePort) }</span></code>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
if data.IcebergPort == 0 && data.LancePort == 0 {
|
|
<div class="alert alert-warning mb-4">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>No catalog endpoint is running. Start `weed s3` with `-s3.port.iceberg` or `-s3.port.lance`, and point admin at it with `-iceberg.port` or `-lance.port`.
|
|
</div>
|
|
}
|
|
<div class="row mb-4">
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
Total Buckets
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{ fmt.Sprintf("%d", data.TotalBuckets) }
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-table icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
Last Updated
|
|
</div>
|
|
<div class="h6 mb-0 font-weight-bold text-gray-800">
|
|
{ data.LastUpdated.Format("2006-01-02 15:04") }
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-clock icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4 col-md-6 mb-4">
|
|
<div class="card border-left-success shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
|
Iceberg REST Port
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
if data.IcebergPort > 0 {
|
|
{ fmt.Sprintf("%d", data.IcebergPort) }
|
|
} else {
|
|
-
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="bi bi-snow icon-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="bi bi-table me-2"></i>Table Buckets
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" width="100%" cellspacing="0" id="s3tablesBucketsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Format</th>
|
|
<th>Owner</th>
|
|
<th>ARN</th>
|
|
<th>Catalog Endpoint</th>
|
|
<th>Created</th>
|
|
<th>Policy</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, bucket := range data.Buckets {
|
|
<tr>
|
|
<td>{ bucket.Name }</td>
|
|
<td>
|
|
<span class={ formatBadgeClass(bucket.Format) } title={ formatBadgeTitle(bucket.Format) }>{ formatLabel(bucket.Format) }</span>
|
|
if s3vectors.IsVectorBucketFormat(bucket.Format) {
|
|
<a class="ms-1 small" href={ dash.PUrl(ctx, "/object-store/vector-buckets/"+url.PathEscape(bucket.Name)) } title="This bucket's S3 Vectors indexes and policy">vector bucket</a>
|
|
}
|
|
</td>
|
|
<td>{ bucket.OwnerAccountID }</td>
|
|
<td class="text-muted small">{ bucket.ARN }</td>
|
|
<td><code class="small">{ bucketCatalogPath(bucket.Format, bucket.Name) }</code></td>
|
|
<td>{ bucket.CreatedAt.Format("2006-01-02 15:04") }</td>
|
|
<td>
|
|
if bucket.PolicyStatementCount > 0 {
|
|
<button type="button" class="badge bg-primary border-0 s3tables-bucket-policy-btn"
|
|
data-bucket-arn={ bucket.ARN } title="View Bucket Policy">
|
|
if bucket.PolicyStatementCount == 1 {
|
|
1 statement
|
|
} else {
|
|
{ fmt.Sprintf("%d statements", bucket.PolicyStatementCount) }
|
|
}
|
|
</button>
|
|
} else {
|
|
<span class="text-muted">Not configured</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
{{ bucketName, parseErr := s3tables.ParseBucketNameFromARN(bucket.ARN) }}
|
|
if parseErr == nil {
|
|
<a class="btn btn-outline-primary btn-sm" href={ dash.PUrl(ctx, fmt.Sprintf("/object-store/s3tables/buckets/%s/namespaces", url.PathEscape(bucketName))) }>
|
|
<i class="bi bi-folder2-open"></i>
|
|
</a>
|
|
} else {
|
|
<button type="button" class="btn btn-outline-primary btn-sm" disabled title="Invalid bucket ARN">
|
|
<i class="bi bi-folder2-open"></i>
|
|
</button>
|
|
}
|
|
<button type="button" class="btn btn-outline-success btn-sm s3tables-tags-btn" data-resource-arn={ bucket.ARN } title="Tags">
|
|
<i class="bi bi-tags"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm s3tables-bucket-policy-btn" data-bucket-arn={ bucket.ARN } title="Bucket Policy">
|
|
<i class="bi bi-shield"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm s3tables-delete-bucket-btn" data-bucket-arn={ bucket.ARN } 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="8" class="text-center text-muted py-4">
|
|
<i class="bi bi-table icon-3x mb-3 text-muted"></i>
|
|
<div>
|
|
<h5>No table buckets found</h5>
|
|
<p>Create your first S3 Tables bucket to get started.</p>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createS3TablesBucketModal">
|
|
<i class="bi bi-plus me-1"></i>Create Bucket
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
if data.IcebergPort > 0 || data.LancePort > 0 {
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<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-code me-2"></i>Client Examples
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
if data.IcebergPort > 0 {
|
|
<h6><span class="badge bg-primary me-2">ICEBERG</span>DuckDB</h6>
|
|
<pre class="bg-light p-3 border rounded">
|
|
<code id="s3tables-duckdb-example">
|
|
{ `INSTALL iceberg;
|
|
LOAD iceberg;
|
|
|
|
CREATE SECRET (
|
|
TYPE ICEBERG,
|
|
ENDPOINT 'http://localhost:` + fmt.Sprintf("%d", data.IcebergPort) + `',
|
|
SCOPE 's3://my-table-bucket/'
|
|
);
|
|
|
|
SELECT * FROM iceberg_scan('s3://my-table-bucket/my-namespace/my-table');` }
|
|
</code>
|
|
</pre>
|
|
<h6 class="mt-4"><span class="badge bg-primary me-2">ICEBERG</span>Python (PyIceberg)</h6>
|
|
<pre class="bg-light p-3 border rounded">
|
|
<code id="s3tables-pyiceberg-example">
|
|
{ `from pyiceberg.catalog import load_catalog
|
|
|
|
catalog = load_catalog(
|
|
name="seaweedfs",
|
|
**{
|
|
"type": "rest",
|
|
"uri": "http://localhost:` + fmt.Sprintf("%d", data.IcebergPort) + `",
|
|
"warehouse": "s3://my-table-bucket/",
|
|
}
|
|
)
|
|
|
|
namespaces = catalog.list_namespaces()` }
|
|
</code>
|
|
</pre>
|
|
}
|
|
if data.LancePort > 0 {
|
|
<h6 class={ templ.KV("mt-4", data.IcebergPort > 0) }><span class="badge bg-success me-2">LANCE</span>Python (lance-namespace)</h6>
|
|
<pre class="bg-light p-3 border rounded">
|
|
<code id="s3tables-lance-namespace-example">
|
|
{ `from lance_namespace import connect
|
|
|
|
ns = connect("rest", {"uri": "http://localhost:` + fmt.Sprintf("%d", data.LancePort) + `"})
|
|
|
|
ns.list_namespaces(id=["my-table-bucket"])
|
|
ns.describe_table(id=["my-table-bucket", "my-namespace", "my-table"])` }
|
|
</code>
|
|
</pre>
|
|
<h6 class="mt-4"><span class="badge bg-success me-2">LANCE</span>Python (pylance)</h6>
|
|
<pre class="bg-light p-3 border rounded">
|
|
<code id="s3tables-pylance-example">
|
|
{ `import lance
|
|
from lance_namespace import connect
|
|
|
|
ns = connect("rest", {"uri": "http://localhost:` + fmt.Sprintf("%d", data.LancePort) + `"})
|
|
table = ns.describe_table(id=["my-table-bucket", "my-namespace", "my-table"])
|
|
|
|
# The namespace vends both the location and the credentials to read it.
|
|
dataset = lance.dataset(table.location, storage_options=table.storage_options)` }
|
|
</code>
|
|
</pre>
|
|
}
|
|
<script>
|
|
const s3tablesExamplesHost = window.location.hostname;
|
|
const s3tablesExamples = [
|
|
document.getElementById('s3tables-duckdb-example'),
|
|
document.getElementById('s3tables-pyiceberg-example'),
|
|
document.getElementById('s3tables-lance-namespace-example'),
|
|
document.getElementById('s3tables-pylance-example')
|
|
];
|
|
s3tablesExamples.forEach(example => {
|
|
if (!example) {
|
|
return;
|
|
}
|
|
example.textContent = example.textContent.replaceAll('localhost', s3tablesExamplesHost);
|
|
});
|
|
</script>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="modal fade" id="createS3TablesBucketModal" tabindex="-1" aria-labelledby="createS3TablesBucketModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="createS3TablesBucketModalLabel">
|
|
<i class="bi bi-plus me-2"></i>Create Table Bucket
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="createS3TablesBucketForm">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="s3tablesBucketName" class="form-label">Bucket Name</label>
|
|
<input type="text" class="form-control" id="s3tablesBucketName" name="name" placeholder="table-bucket-name" required/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Table Format</label>
|
|
<div class="row g-2" id="s3tablesBucketFormatPicker">
|
|
<div class="col-6">
|
|
if data.IcebergPort > 0 {
|
|
<input type="radio" class="btn-check" name="format" id="s3tablesBucketFormatIceberg" value="ICEBERG" checked/>
|
|
} else {
|
|
<input type="radio" class="btn-check" name="format" id="s3tablesBucketFormatIceberg" value="ICEBERG" disabled/>
|
|
}
|
|
<label class="btn btn-outline-primary w-100 text-start p-3" for="s3tablesBucketFormatIceberg">
|
|
<span class="d-block fw-semibold"><i class="bi bi-snow me-1"></i>Iceberg</span>
|
|
<span class="d-block small text-muted mt-1">Served over the Iceberg REST catalog. Spark, Trino, PyIceberg, DuckDB, OLake Go.</span>
|
|
</label>
|
|
</div>
|
|
<div class="col-6">
|
|
if data.LancePort > 0 && data.IcebergPort <= 0 {
|
|
// The only format this cluster serves, so it is the one preselected.
|
|
<input type="radio" class="btn-check" name="format" id="s3tablesBucketFormatLance" value="LANCE" checked/>
|
|
} else if data.LancePort > 0 {
|
|
<input type="radio" class="btn-check" name="format" id="s3tablesBucketFormatLance" value="LANCE"/>
|
|
} else {
|
|
<input type="radio" class="btn-check" name="format" id="s3tablesBucketFormatLance" value="LANCE" disabled/>
|
|
}
|
|
<label class="btn btn-outline-success w-100 text-start p-3" for="s3tablesBucketFormatLance">
|
|
<span class="d-block fw-semibold"><i class="bi bi-bounding-box me-1"></i>Lance</span>
|
|
<span class="d-block small text-muted mt-1">Served over the Lance Namespace API. pylance, LanceDB, Ray.</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-text" id="s3tablesBucketFormatHint" data-iceberg-port={ fmt.Sprintf("%d", data.IcebergPort) } data-lance-port={ fmt.Sprintf("%d", data.LancePort) }>
|
|
A bucket holds one format. Tables of the other are refused.
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="s3tablesBucketOwner" class="form-label">Owner (Optional)</label>
|
|
<select class="form-select" id="s3tablesBucketOwner" name="owner">
|
|
<option value="">No owner (admin-only access)</option>
|
|
</select>
|
|
<div class="form-text">
|
|
The S3 identity that owns this table bucket. Non-admin users can only access table buckets they own.
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="s3tablesBucketTags" class="form-label">Tags</label>
|
|
<input type="text" class="form-control" id="s3tablesBucketTags" name="tags" placeholder="key1=value1,key2=value2"/>
|
|
<div class="form-text">Optional tags in key=value format.</div>
|
|
</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="deleteS3TablesBucketModal" tabindex="-1" aria-labelledby="deleteS3TablesBucketModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="deleteS3TablesBucketModalLabel">
|
|
<i class="bi bi-exclamation-triangle me-2 text-warning"></i>Delete Table Bucket
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete the table bucket <strong id="deleteS3TablesBucketName"></strong>?</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" onclick="deleteS3TablesBucket()">
|
|
<i class="bi bi-trash me-1"></i>Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@PolicyDatalists()
|
|
|
|
<div class="modal fade" id="s3tablesBucketPolicyModal" tabindex="-1" aria-labelledby="s3tablesBucketPolicyModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="s3tablesBucketPolicyModalLabel">
|
|
<i class="bi bi-shield me-2"></i>Table Bucket Policy
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="s3tablesBucketPolicyForm">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="s3tablesBucketPolicyArn" name="bucket_arn"/>
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="s3tablesBucketPolicyEditorTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#s3tablesBucketPolicyEditorTab" role="tab" aria-controls="s3tablesBucketPolicyEditorTab" aria-selected="true">Editor</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="s3tablesBucketPolicyJsonTabBtn" type="button" data-bs-toggle="tab" data-bs-target="#s3tablesBucketPolicyJsonTab" role="tab" aria-controls="s3tablesBucketPolicyJsonTab" aria-selected="false">JSON</button>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content border border-top-0 rounded-bottom p-3 mb-3">
|
|
<div class="tab-pane fade show active" id="s3tablesBucketPolicyEditorTab" role="tabpanel" aria-labelledby="s3tablesBucketPolicyEditorTabBtn">
|
|
<div id="s3tablesBucketPolicyEditorBody"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="s3tablesBucketPolicyAddStatementBtn">
|
|
<i class="bi bi-plus me-1"></i>Add statement
|
|
</button>
|
|
</div>
|
|
<div class="tab-pane fade" id="s3tablesBucketPolicyJsonTab" role="tabpanel" aria-labelledby="s3tablesBucketPolicyJsonTabBtn">
|
|
<textarea class="form-control" id="s3tablesBucketPolicyText" name="policy" rows="12" placeholder="{ }"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="form-text">
|
|
Provide a policy JSON; use Delete Policy to remove the policy.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-outline-danger" id="s3tablesBucketPolicyDeleteBtn" onclick="deleteS3TablesBucketPolicy()">
|
|
<i class="bi bi-trash me-1"></i>Delete Policy
|
|
</button>
|
|
<!-- type="button": a submit button would make Enter in any
|
|
single-line editor input implicitly submit a half-built
|
|
policy, which the backend stores verbatim. -->
|
|
<button type="button" class="btn btn-primary" id="s3tablesBucketPolicySaveBtn">
|
|
<i class="bi bi-floppy me-1"></i>Save Policy
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal fade" id="s3tablesTagsModal" tabindex="-1" aria-labelledby="s3tablesTagsModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="s3tablesTagsModalLabel">
|
|
<i class="bi bi-tags me-2"></i>Resource Tags
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="s3tablesTagsForm">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="s3tablesTagsResourceArn" name="resource_arn"/>
|
|
<div class="mb-3">
|
|
<label class="form-label">Existing Tags</label>
|
|
<pre class="bg-light p-3 border rounded" id="s3tablesTagsList">Loading...</pre>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="s3tablesTagsInput" class="form-label">Add or Update Tags</label>
|
|
<input type="text" class="form-control" id="s3tablesTagsInput" placeholder="key1=value1,key2=value2"/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="s3tablesTagsDeleteInput" class="form-label">Remove Tag Keys</label>
|
|
<input type="text" class="form-control" id="s3tablesTagsDeleteInput" placeholder="key1,key2"/>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-outline-danger" onclick="deleteS3TablesTags()">
|
|
<i class="bi bi-trash me-1"></i>Remove Tags
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-floppy me-1"></i>Update Tags
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initS3TablesBuckets();
|
|
});
|
|
</script>
|
|
}
|