mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: add explore button for object storage resources (#10306)
* feat: add explore button for object storage resources in resource list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FhmfSxPuTck3yAhDpkfcA * fix: make s3 drawer tooltip reflect explored resource Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FhmfSxPuTck3yAhDpkfcA * fix: honor workspace prop in global s3 explorer and add resource connection error state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FhmfSxPuTck3yAhDpkfcA * fix: use picker's effective workspace in S3FilePreview requests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FhmfSxPuTck3yAhDpkfcA * fix: pass acting workspace to explore button in ResourcePicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FhmfSxPuTck3yAhDpkfcA * chore: update ee-repo-ref to f78df23339e3136e8b6e9148a509508633448dd2 This commit updates the EE repository reference after PR #686 was merged in windmill-ee-private. Previous ee-repo-ref: efb5e014fec34fc580b9dbb1b260494dd76c5462 New ee-repo-ref: f78df23339e3136e8b6e9148a509508633448dd2 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
22abd6d4e229f1206a13ebee8a6a9b808cd82a0d
|
||||
f78df23339e3136e8b6e9148a509508633448dd2
|
||||
|
||||
@@ -20889,6 +20889,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, test the connection of this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Connection settings
|
||||
@@ -20970,6 +20975,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, list the files of this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: List of file keys
|
||||
@@ -21006,6 +21016,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, load the file metadata from this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: FileMetadata
|
||||
@@ -21055,6 +21070,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, load the file preview from this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: FilePreview
|
||||
@@ -21365,6 +21385,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, delete the file from this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Confirmation
|
||||
@@ -21394,6 +21419,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
description: When set, move the file within this object storage resource instead of the workspace storage
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Confirmation
|
||||
|
||||
@@ -4191,15 +4191,19 @@ async fn app_load_file_metadata(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<LoadFileMetadataQuery>,
|
||||
Query(mut query): Query<LoadFileMetadataQuery>,
|
||||
Query(sig): Query<AppS3Sig>,
|
||||
) -> Result<Response> {
|
||||
let path = path.to_path();
|
||||
let file_query = app_s3_file_query(query.file_key.clone(), query.storage.clone(), sig);
|
||||
let job_authed =
|
||||
app_s3_on_behalf_and_provenance(&db, &path, &w_id, &opt_authed, &file_query).await?;
|
||||
// On-behalf app reads are confined to the workspace storage; a
|
||||
// viewer-supplied custom resource must not be honored.
|
||||
query.s3_resource_path = None;
|
||||
let resp =
|
||||
crate::job_helpers_oss::load_file_metadata_internal(job_authed, &db, &w_id, query).await?;
|
||||
crate::job_helpers_oss::load_file_metadata_internal(job_authed, &db, None, &w_id, query)
|
||||
.await?;
|
||||
Ok(Json(resp).into_response())
|
||||
}
|
||||
|
||||
@@ -4208,15 +4212,19 @@ async fn app_load_file_preview(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<LoadFilePreviewQuery>,
|
||||
Query(mut query): Query<LoadFilePreviewQuery>,
|
||||
Query(sig): Query<AppS3Sig>,
|
||||
) -> Result<Response> {
|
||||
let path = path.to_path();
|
||||
let file_query = app_s3_file_query(query.file_key.clone(), query.storage.clone(), sig);
|
||||
let job_authed =
|
||||
app_s3_on_behalf_and_provenance(&db, &path, &w_id, &opt_authed, &file_query).await?;
|
||||
// On-behalf app reads are confined to the workspace storage; a
|
||||
// viewer-supplied custom resource must not be honored.
|
||||
query.s3_resource_path = None;
|
||||
let resp =
|
||||
crate::job_helpers_oss::load_file_preview_internal(job_authed, &db, &w_id, query).await?;
|
||||
crate::job_helpers_oss::load_file_preview_internal(job_authed, &db, None, &w_id, query)
|
||||
.await?;
|
||||
Ok(Json(resp).into_response())
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ pub async fn read_object_streamable(
|
||||
pub async fn delete_s3_file_internal(
|
||||
_authed: OptJobAuthed,
|
||||
_db: &DB,
|
||||
_token: &str,
|
||||
_user_db: Option<windmill_common::db::UserDB>,
|
||||
_w_id: &str,
|
||||
_query: DeleteS3FileQuery,
|
||||
) -> error::Result<()> {
|
||||
@@ -215,6 +215,7 @@ pub async fn delete_s3_file_internal(
|
||||
pub struct DeleteS3FileQuery {
|
||||
pub file_key: String,
|
||||
pub storage: Option<String>,
|
||||
pub s3_resource_path: Option<String>,
|
||||
}
|
||||
|
||||
// Stubs for the app-scoped S3 display ops (mirrors the EE `*_internal` helpers +
|
||||
@@ -231,6 +232,7 @@ mod app_s3_display_stubs {
|
||||
pub struct LoadFileMetadataQuery {
|
||||
pub file_key: String,
|
||||
pub storage: Option<String>,
|
||||
pub s3_resource_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -242,6 +244,7 @@ mod app_s3_display_stubs {
|
||||
#[allow(dead_code)]
|
||||
pub struct LoadFilePreviewQuery {
|
||||
pub storage: Option<String>,
|
||||
pub s3_resource_path: Option<String>,
|
||||
pub file_key: String,
|
||||
pub file_size_in_bytes: Option<u64>,
|
||||
pub file_mime_type: Option<String>,
|
||||
@@ -281,6 +284,7 @@ mod app_s3_display_stubs {
|
||||
pub async fn load_file_metadata_internal(
|
||||
_authed: OptJobAuthed,
|
||||
_db: &DB,
|
||||
_user_db: Option<UserDB>,
|
||||
_w_id: &str,
|
||||
_query: LoadFileMetadataQuery,
|
||||
) -> error::Result<LoadFileMetadataResponse> {
|
||||
@@ -292,6 +296,7 @@ mod app_s3_display_stubs {
|
||||
pub async fn load_file_preview_internal(
|
||||
_authed: OptJobAuthed,
|
||||
_db: &DB,
|
||||
_user_db: Option<UserDB>,
|
||||
_w_id: &str,
|
||||
_query: LoadFilePreviewQuery,
|
||||
) -> error::Result<LoadFilePreviewResponse> {
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
<script module lang="ts">
|
||||
const OBJECT_STORAGE_RESOURCE_TYPES = [
|
||||
's3',
|
||||
'azure_blob',
|
||||
's3_aws_oidc',
|
||||
'azure_workload_identity',
|
||||
'gcloud_storage'
|
||||
]
|
||||
|
||||
export function isObjectStorageResourceType(resourceType: string | undefined): boolean {
|
||||
return resourceType !== undefined && OBJECT_STORAGE_RESOURCE_TYPES.includes(resourceType)
|
||||
}
|
||||
|
||||
export function assetCanBeExplored(
|
||||
asset: Asset,
|
||||
_resourceMetadata?: { resource_type?: string }
|
||||
@@ -8,7 +20,9 @@
|
||||
asset.kind === 'datatable' ||
|
||||
asset.kind === 's3object' ||
|
||||
asset.kind === 'volume' ||
|
||||
(asset.kind === 'resource' && isDbType(_resourceMetadata?.resource_type))
|
||||
(asset.kind === 'resource' &&
|
||||
(isDbType(_resourceMetadata?.resource_type) ||
|
||||
isObjectStorageResourceType(_resourceMetadata?.resource_type)))
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -19,7 +33,12 @@
|
||||
import { Button, ButtonType } from '$lib/components/common'
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import { VolumeService } from '$lib/gen'
|
||||
import { globalDbManagerDrawer, userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
globalDbManagerDrawer,
|
||||
globalS3FilePickerExplorer,
|
||||
userStore,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import { isS3Uri } from '$lib/utils'
|
||||
import { Database, File, HardDriveIcon } from 'lucide-svelte'
|
||||
import DucklakeIcon from './icons/DucklakeIcon.svelte'
|
||||
@@ -52,6 +71,12 @@
|
||||
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
const assetUri = $derived(formatAsset(asset))
|
||||
// Contexts with a select/upload flow pass their own picker; everything else
|
||||
// (e.g. the resources list) falls back to the global read-only explorer.
|
||||
let effectiveS3FilePicker = $derived(s3FilePicker ?? globalS3FilePickerExplorer.val)
|
||||
let isStorageResource = $derived(
|
||||
asset.kind === 'resource' && isObjectStorageResourceType(_resourceMetadata?.resource_type)
|
||||
)
|
||||
</script>
|
||||
|
||||
<Button
|
||||
@@ -73,11 +98,16 @@
|
||||
},
|
||||
ws
|
||||
)
|
||||
} else if (isStorageResource) {
|
||||
effectiveS3FilePicker?.open(undefined, { s3ResourcePath: asset.path, workspace: ws })
|
||||
} else if (asset.kind === 's3object' && isS3Uri(assetUri)) {
|
||||
s3FilePicker?.open(assetUri)
|
||||
effectiveS3FilePicker?.open(assetUri, { workspace: ws })
|
||||
} else if (asset.kind === 'volume') {
|
||||
const storage = (await VolumeService.getVolumeStorage({ workspace: ws! })) ?? undefined
|
||||
s3FilePicker?.open({ s3: `volumes/${ws}/${asset.path}/`, storage })
|
||||
effectiveS3FilePicker?.open(
|
||||
{ s3: `volumes/${ws}/${asset.path}/`, storage },
|
||||
{ workspace: ws }
|
||||
)
|
||||
} else if (asset.kind === 'ducklake') {
|
||||
let ducklake = asset.path.split('/')[0]
|
||||
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
|
||||
@@ -106,7 +136,7 @@
|
||||
}
|
||||
onClick?.()
|
||||
}}
|
||||
endIcon={asset.kind === 's3object'
|
||||
endIcon={asset.kind === 's3object' || isStorageResource
|
||||
? { icon: File }
|
||||
: asset.kind === 'resource' || asset.kind === 'datatable'
|
||||
? { icon: Database }
|
||||
@@ -116,7 +146,7 @@
|
||||
? { icon: HardDriveIcon }
|
||||
: undefined}
|
||||
>
|
||||
{#if asset.kind === 's3object' || asset.kind === 'volume'}
|
||||
{#if asset.kind === 's3object' || asset.kind === 'volume' || isStorageResource}
|
||||
<span class:hidden={noText}>Explore</span>
|
||||
{:else if asset.kind === 'resource' || asset.kind === 'ducklake' || asset.kind === 'datatable'}
|
||||
<span class:hidden={noText}>Manage</span>
|
||||
|
||||
@@ -331,6 +331,7 @@
|
||||
class="mt-1"
|
||||
_resourceMetadata={{ resource_type: resourceType }}
|
||||
asset={{ kind: 'resource', path: value }}
|
||||
workspace={effectiveWorkspace}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -38,13 +38,17 @@
|
||||
onSelectAndClose
|
||||
}: Props = $props()
|
||||
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let s3FilePickerInner: S3FilePickerInner | undefined = $state()
|
||||
|
||||
let workspaceSettingsInitialized = $state(true)
|
||||
let storage: string | undefined = $state(undefined)
|
||||
let s3ResourcePath: string | undefined = $state(undefined)
|
||||
/** Per-open workspace override, for callers (e.g. the global explorer) whose
|
||||
* asset lives in a different workspace than this picker was mounted for. */
|
||||
let workspaceOverride: string | undefined = $state(undefined)
|
||||
let effectiveWorkspace = $derived(workspaceOverride ?? workspace)
|
||||
let ws = $derived(effectiveWorkspace ?? $workspaceStore)
|
||||
let uploadModalOpen = $state(false)
|
||||
|
||||
let allFilesByKey: Record<
|
||||
@@ -66,8 +70,15 @@
|
||||
{ lazy: true }
|
||||
)
|
||||
|
||||
export async function open(_preSelectedFileKey: S3Object | undefined = undefined) {
|
||||
secondaryStorageNames.refetch()
|
||||
export async function open(
|
||||
_preSelectedFileKey: S3Object | undefined = undefined,
|
||||
opts: { s3ResourcePath?: string; workspace?: string } = {}
|
||||
) {
|
||||
s3ResourcePath = opts.s3ResourcePath
|
||||
workspaceOverride = opts.workspace
|
||||
if (!s3ResourcePath) {
|
||||
secondaryStorageNames.refetch()
|
||||
}
|
||||
drawer?.openDrawer?.()
|
||||
|
||||
await tick()
|
||||
@@ -86,12 +97,14 @@
|
||||
size="1200px"
|
||||
>
|
||||
<DrawerContent
|
||||
title="S3 file browser"
|
||||
title={s3ResourcePath ? `Exploring ${s3ResourcePath}` : 'S3 file browser'}
|
||||
on:close={() => {
|
||||
s3FilePickerInner?.exit?.()
|
||||
drawer?.closeDrawer?.()
|
||||
}}
|
||||
tooltip="Files present in the Workspace S3 bucket. You can set the workspace S3 bucket in the settings."
|
||||
tooltip={s3ResourcePath
|
||||
? `Files present in the bucket of the ${s3ResourcePath} resource.`
|
||||
: 'Files present in the Workspace S3 bucket. You can set the workspace S3 bucket in the settings.'}
|
||||
documentationLink="https://www.windmill.dev/docs/integrations/s3"
|
||||
>
|
||||
<S3FilePickerInner
|
||||
@@ -109,13 +122,14 @@
|
||||
bind:storage
|
||||
bind:allFilesByKey
|
||||
bind:uploadModalOpen
|
||||
{s3ResourcePath}
|
||||
{folderOnly}
|
||||
{regexFilter}
|
||||
{workspace}
|
||||
workspace={effectiveWorkspace}
|
||||
/>
|
||||
{#snippet actions()}
|
||||
<div class="flex gap-1">
|
||||
{#if secondaryStorageNames.current?.length}
|
||||
{#if !s3ResourcePath && secondaryStorageNames.current?.length}
|
||||
<Select
|
||||
inputClass="h-10 min-w-44 !placeholder-secondary"
|
||||
items={[
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
workspace?: string | undefined
|
||||
workspaceSettingsInitialized?: boolean
|
||||
storage?: string | undefined
|
||||
/** Browse this object storage resource directly instead of the workspace storage. */
|
||||
s3ResourcePath?: string | undefined
|
||||
uploadModalOpen?: boolean
|
||||
allFilesByKey?: Record<
|
||||
string,
|
||||
@@ -108,6 +110,7 @@
|
||||
workspace = undefined,
|
||||
workspaceSettingsInitialized = $bindable(true),
|
||||
storage = $bindable(undefined),
|
||||
s3ResourcePath = undefined,
|
||||
uploadModalOpen = $bindable(false),
|
||||
allFilesByKey = $bindable({}),
|
||||
allowDelete = false,
|
||||
@@ -192,7 +195,8 @@
|
||||
maxKeys: maxKeys, // fixed pages of 1000 files for now
|
||||
marker: page == 0 ? undefined : listMarkers[page - 1],
|
||||
prefix: rootPath ?? (filter.trim() != '' ? filter : undefined),
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
if (
|
||||
availableFiles.restricted_access === null ||
|
||||
@@ -287,7 +291,8 @@
|
||||
let fileMetadataRaw = await loadFileMetadataRequest({
|
||||
workspace: ws!,
|
||||
fileKey: fileKey,
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
|
||||
if (fileMetadataRaw !== undefined) {
|
||||
@@ -313,7 +318,8 @@
|
||||
csvHasHeader: csvHasHeader,
|
||||
readBytesFrom: 0,
|
||||
readBytesLength: 128 * 1024, // For now static limit of 128Kb per file,
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
|
||||
let filePreviewContent = filePreviewRaw.content
|
||||
@@ -356,7 +362,8 @@
|
||||
await deleteS3FileRequest({
|
||||
workspace: ws!,
|
||||
fileKey: fileKey,
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
} finally {
|
||||
fileDeletionInProgress = false
|
||||
@@ -417,7 +424,8 @@
|
||||
workspace: ws!,
|
||||
srcFileKey: srcFileKey,
|
||||
destFileKey: destFileKey!,
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
} finally {
|
||||
fileMoveInProgress = false
|
||||
@@ -463,7 +471,8 @@
|
||||
try {
|
||||
await testConnectionRequest({
|
||||
workspace: ws!,
|
||||
storage: storage
|
||||
storage: storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
workspaceSettingsInitialized = true
|
||||
} catch (e) {
|
||||
@@ -554,6 +563,15 @@
|
||||
<p class="text-clip grow min-w-0"> Double check the S3 resource fields and try again. </p>
|
||||
</div>
|
||||
</Alert>
|
||||
{:else if s3ResourcePath}
|
||||
<Alert type="error" title="Could not connect to the object storage of {s3ResourcePath}">
|
||||
<div class="flex flex-row gap-x-1 w-full items-center">
|
||||
<p class="text-clip grow min-w-0">
|
||||
Double check the resource fields and that its object storage is reachable, then try again.
|
||||
</p>
|
||||
<Button variant="default" on:click={reloadContent} startIcon={{ icon: RotateCw }} />
|
||||
</div>
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert type="error" title="Workspace not connected to any S3 storage">
|
||||
<div class="flex flex-row gap-x-1 w-full items-center">
|
||||
@@ -721,7 +739,7 @@
|
||||
{#if filePreview !== undefined && (!hideS3SpecificDetails || !readOnlyMode || allowDelete)}
|
||||
<div class="flex gap-2 shrink-0">
|
||||
{#if !hideS3SpecificDetails}
|
||||
{@const downloadApiPath = `/w/${ws}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`}
|
||||
{@const downloadApiPath = `/w/${ws}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}${s3ResourcePath ? `&s3_resource_path=${encodeURIComponent(s3ResourcePath)}` : ''}`}
|
||||
{@const downloadName =
|
||||
fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
|
||||
{#if shouldDownloadViaClient()}
|
||||
@@ -789,6 +807,8 @@
|
||||
<S3FilePreview
|
||||
fileKey={fileMetadata?.fileKey}
|
||||
{storage}
|
||||
{s3ResourcePath}
|
||||
workspace={ws}
|
||||
{loadFilePreviewRequest}
|
||||
{loadFileMetadataRequest}
|
||||
class="h-full"
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
// Optional override of the storage backend (matches the picker's
|
||||
// `storage` prop). Empty/undefined uses the workspace default.
|
||||
storage?: string | undefined
|
||||
// Browse this object storage resource directly instead of the
|
||||
// workspace storage (matches the picker's `s3ResourcePath` prop).
|
||||
s3ResourcePath?: string | undefined
|
||||
// Workspace the previewed object lives in — the acting workspace of the
|
||||
// surface that opened the preview; defaults to the nav workspace.
|
||||
workspace?: string | undefined
|
||||
// Override hooks for non-default backends (e.g. workspace settings
|
||||
// preview before the storage is committed). Default to the standard
|
||||
// helpers service — same defaults as S3FilePickerInner.
|
||||
@@ -48,6 +54,8 @@
|
||||
let {
|
||||
fileKey,
|
||||
storage = undefined,
|
||||
s3ResourcePath = undefined,
|
||||
workspace = undefined,
|
||||
loadFilePreviewRequest = HelpersService.loadFilePreview,
|
||||
loadFileMetadataRequest = HelpersService.loadFileMetadata,
|
||||
showMetadata = false,
|
||||
@@ -103,11 +111,13 @@
|
||||
// existence after an upstream run completes — moving from the
|
||||
// "not yet materialized" empty state to the actual preview without
|
||||
// requiring the user to re-click the asset.
|
||||
let ws = $derived(workspace ?? $workspaceStore)
|
||||
|
||||
$effect(() => {
|
||||
const key = fileKey
|
||||
const ws = $workspaceStore
|
||||
const ws_ = ws
|
||||
void refreshKey
|
||||
if (!key || !ws) {
|
||||
if (!key || !ws_) {
|
||||
fileMetadata = undefined
|
||||
filePreview = undefined
|
||||
notFound = false
|
||||
@@ -129,9 +139,10 @@
|
||||
loadError = undefined
|
||||
try {
|
||||
const meta = await loadFileMetadataRequest({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
fileKey: key,
|
||||
storage
|
||||
storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
if (meta !== undefined) {
|
||||
fileMetadata = {
|
||||
@@ -158,7 +169,7 @@
|
||||
filePreviewLoading = true
|
||||
try {
|
||||
const raw = await loadFilePreviewRequest({
|
||||
workspace: $workspaceStore!,
|
||||
workspace: ws!,
|
||||
fileKey: key,
|
||||
fileSizeInBytes: size,
|
||||
fileMimeType: mimeType,
|
||||
@@ -166,7 +177,8 @@
|
||||
csvHasHeader: csvHasHeader,
|
||||
readBytesFrom: 0,
|
||||
readBytesLength: 128 * 1024,
|
||||
storage
|
||||
storage,
|
||||
s3ResourcePath
|
||||
})
|
||||
let content = raw.content
|
||||
if (content !== null && content !== undefined && content.length >= 128 * 1024) {
|
||||
@@ -191,8 +203,12 @@
|
||||
}
|
||||
|
||||
// `storage` is keyed by the workspace's S3 storage config name — used as
|
||||
// a query-string suffix on the image/PDF preview URLs.
|
||||
let storageQS = $derived(storage ? `&storage=${storage}` : '')
|
||||
// a query-string suffix on the image/PDF preview URLs, together with the
|
||||
// optional custom resource override.
|
||||
let storageQS = $derived(
|
||||
(storage ? `&storage=${storage}` : '') +
|
||||
(s3ResourcePath ? `&s3_resource_path=${encodeURIComponent(s3ResourcePath)}` : '')
|
||||
)
|
||||
|
||||
function onCsvControlsChanged() {
|
||||
if (fileMetadata?.fileKey) {
|
||||
@@ -245,7 +261,7 @@
|
||||
{:else if fileMetadata?.fileKey.endsWith('.png') || fileMetadata?.fileKey.endsWith('.jpg') || fileMetadata?.fileKey.endsWith('.jpeg') || fileMetadata?.fileKey.endsWith('.webp')}
|
||||
<div>
|
||||
<ExpandableImage
|
||||
src={`/api/w/${$workspaceStore}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
|
||||
src={`/api/w/${ws}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
|
||||
fileMetadata.fileKey
|
||||
)}${storageQS}`}
|
||||
alt="S3 preview"
|
||||
@@ -258,7 +274,7 @@
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
source={`/api/w/${$workspaceStore}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
|
||||
source={`/api/w/${ws}/job_helpers/load_image_preview?file_key=${encodeURIComponent(
|
||||
fileMetadata.fileKey
|
||||
)}${storageQS}`}
|
||||
/>
|
||||
|
||||
@@ -146,6 +146,15 @@ export const aiUserDisabled = writable<boolean>(
|
||||
export const usedTriggerKinds = writable<string[]>([])
|
||||
|
||||
export let globalDbManagerDrawer: StateStore<DbManagerUriState | undefined> = { val: undefined }
|
||||
/** Read-only S3 file browser (S3FilePicker instance) mounted in the logged
|
||||
* layout, used by Explore buttons in contexts that don't wire their own picker
|
||||
* instance. Typed loosely because the component instance type resolves
|
||||
* differently in .ts and .svelte contexts. */
|
||||
export let globalS3FilePickerExplorer: StateStore<
|
||||
{ open: (fileKey?: any, opts?: { s3ResourcePath?: string }) => Promise<void> } | undefined
|
||||
> = createState({
|
||||
val: undefined
|
||||
})
|
||||
export let globalForkModal: StateStore<GlobalForkModalState | undefined> = createState({
|
||||
val: undefined
|
||||
})
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
devopsRole,
|
||||
whitelabelNameStore,
|
||||
globalDbManagerDrawer,
|
||||
globalForkModal
|
||||
globalForkModal,
|
||||
globalS3FilePickerExplorer
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation'
|
||||
@@ -83,6 +84,7 @@
|
||||
import WorkspaceScopeHeader from '$lib/components/sidebar/WorkspaceScopeHeader.svelte'
|
||||
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
|
||||
import DBManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import { useIsDarkMode } from '$lib/components/DarkModeObserver.svelte'
|
||||
import { useDbManagerUriState } from '$lib/components/dbManagerDrawerModel.svelte'
|
||||
import Modal2 from '$lib/components/common/modal/Modal2.svelte'
|
||||
@@ -778,6 +780,13 @@
|
||||
})
|
||||
|
||||
globalDbManagerDrawer.val = useDbManagerUriState()
|
||||
|
||||
let globalS3FilePicker: S3FilePicker | undefined = $state()
|
||||
$effect(() => {
|
||||
// `as any`: the component instance type is opaque in svelte2tsx context
|
||||
// and does not match the store's structural type.
|
||||
globalS3FilePickerExplorer.val = globalS3FilePicker as any
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
@@ -1327,6 +1336,10 @@
|
||||
<DBManagerDrawer uriState={globalDbManagerDrawer.val} />
|
||||
{/if}
|
||||
|
||||
{#if $workspaceStore}
|
||||
<S3FilePicker bind:this={globalS3FilePicker} readOnlyMode allowDelete />
|
||||
{/if}
|
||||
|
||||
<ForkConflictModal />
|
||||
|
||||
<Modal2
|
||||
|
||||
Reference in New Issue
Block a user