feat: file explorer
All checks were successful
Build and Push App Image / build-and-push (push) Successful in 50s

This commit is contained in:
domrichardson
2026-03-25 11:27:15 +00:00
parent b253bec9fc
commit 168f5eac83
16 changed files with 1297 additions and 20 deletions

View File

@@ -254,6 +254,49 @@
</div>
</div>
<div class="feature-flag-item border rounded p-3">
<div class="d-flex justify-content-between align-items-center mb-0" :class="{ 'mb-3': featureFlagsForm.file_explorer_enabled }">
<div>
<div class="fw-semibold">Enable File Explorer</div>
<div class="small text-muted">Allow users to browse and insert files from an S3 bucket directly into notes.</div>
</div>
<div class="form-check form-switch m-0">
<input id="flag-file-explorer" v-model="featureFlagsForm.file_explorer_enabled" class="form-check-input" type="checkbox" />
</div>
</div>
<div v-if="featureFlagsForm.file_explorer_enabled" class="row g-2 mt-1">
<div class="col-md-6">
<label class="form-label small mb-1">S3 Endpoint URL</label>
<input v-model="featureFlagsForm.s3_endpoint" type="url" class="form-control form-control-sm" placeholder="https://s3.amazonaws.com or custom endpoint" />
</div>
<div class="col-md-6">
<label class="form-label small mb-1">Bucket Name</label>
<input v-model="featureFlagsForm.s3_bucket" type="text" class="form-control form-control-sm" placeholder="my-bucket" />
</div>
<div class="col-md-4">
<label class="form-label small mb-1">Region</label>
<input v-model="featureFlagsForm.s3_region" type="text" class="form-control form-control-sm" placeholder="us-east-1" />
</div>
<div class="col-md-4">
<label class="form-label small mb-1">Access Key</label>
<input v-model="featureFlagsForm.s3_access_key" type="text" class="form-control form-control-sm" autocomplete="off" />
</div>
<div class="col-md-4">
<label class="form-label small mb-1">Secret Key</label>
<input
v-model="featureFlagsForm.s3_secret_key"
type="password"
class="form-control form-control-sm"
:placeholder="featureFlagsForm.s3_secret_key_set ? 'Leave blank to keep current secret' : 'Enter secret key'"
autocomplete="new-password"
/>
<div v-if="featureFlagsForm.s3_secret_key_set && !featureFlagsForm.s3_secret_key" class="small text-success mt-1">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i> Secret key is set
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end">
<button class="btn btn-primary" :disabled="savingFeatureFlags" @click="saveFeatureFlags">
{{ savingFeatureFlags ? "Saving..." : "Save Feature Flags" }}
@@ -364,6 +407,13 @@ const featureFlagsForm = ref({
registration_enabled: true,
provider_login_enabled: true,
public_sharing_enabled: true,
file_explorer_enabled: false,
s3_endpoint: "",
s3_bucket: "",
s3_region: "",
s3_access_key: "",
s3_secret_key: "",
s3_secret_key_set: false,
});
const clearMessages = () => {
@@ -584,6 +634,13 @@ const loadFeatureFlags = async () => {
registration_enabled: !!res.data.registration_enabled,
provider_login_enabled: !!res.data.provider_login_enabled,
public_sharing_enabled: !!res.data.public_sharing_enabled,
file_explorer_enabled: !!res.data.file_explorer_enabled,
s3_endpoint: res.data.s3_endpoint || "",
s3_bucket: res.data.s3_bucket || "",
s3_region: res.data.s3_region || "",
s3_access_key: res.data.s3_access_key || "",
s3_secret_key: "", // never pre-fill the secret
s3_secret_key_set: !!res.data.s3_secret_key_set,
};
} catch (e) {
error.value = e.response?.data || "Failed to load feature flags.";
@@ -596,11 +653,28 @@ const saveFeatureFlags = async () => {
savingFeatureFlags.value = true;
clearMessages();
try {
const res = await apiClient.put("/api/v1/admin/feature-flags", featureFlagsForm.value);
const res = await apiClient.put("/api/v1/admin/feature-flags", {
registration_enabled: featureFlagsForm.value.registration_enabled,
provider_login_enabled: featureFlagsForm.value.provider_login_enabled,
public_sharing_enabled: featureFlagsForm.value.public_sharing_enabled,
file_explorer_enabled: featureFlagsForm.value.file_explorer_enabled,
s3_endpoint: featureFlagsForm.value.s3_endpoint,
s3_bucket: featureFlagsForm.value.s3_bucket,
s3_region: featureFlagsForm.value.s3_region,
s3_access_key: featureFlagsForm.value.s3_access_key,
s3_secret_key: featureFlagsForm.value.s3_secret_key, // blank = keep existing
});
featureFlagsForm.value = {
registration_enabled: !!res.data.registration_enabled,
provider_login_enabled: !!res.data.provider_login_enabled,
public_sharing_enabled: !!res.data.public_sharing_enabled,
file_explorer_enabled: !!res.data.file_explorer_enabled,
s3_endpoint: res.data.s3_endpoint || "",
s3_bucket: res.data.s3_bucket || "",
s3_region: res.data.s3_region || "",
s3_access_key: res.data.s3_access_key || "",
s3_secret_key: "",
s3_secret_key_set: !!res.data.s3_secret_key_set,
};
successMessage.value = "Feature flags updated.";
} catch (e) {