improve DbManagerButton + asset dropdown button

This commit is contained in:
Diego Imbert
2025-06-26 16:57:30 +02:00
parent f816436614
commit c57f96a79e
10 changed files with 254 additions and 116 deletions
@@ -4,7 +4,7 @@
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { sendUserToast, sortArray } from '$lib/utils'
import { ArrowLeft, Database, Expand, Loader2, Minimize, RefreshCcw } from 'lucide-svelte'
import { ArrowLeft, Expand, Loader2, Minimize, RefreshCcw } from 'lucide-svelte'
import {
dbSupportsSchemas,
getDbSchemas,
@@ -15,7 +15,6 @@
type TableMetadata
} from './apps/components/display/dbtable/utils'
import DbManager from './DBManager.svelte'
import { Alert } from './common'
import { dbDeleteTableActionWithPreviewScript, dbTableOpsWithPreviewScripts } from './dbOps'
import { makeCreateTableQuery } from './apps/components/display/dbtable/queries/createTable'
import { runScriptAndPollResult } from './jobs/utils'
@@ -24,22 +23,24 @@
import SimpleAgTable from './SimpleAgTable.svelte'
import { untrack } from 'svelte'
type Props = {
resourceType: DbType
resourcePath: string
class?: string
let resourceType: DbType | undefined = $state(undefined)
let resourcePath: string | undefined = $state(undefined)
let open = $derived(resourcePath && resourceType)
export function openDrawer(_resourceType: DbType, _resourcePath: string) {
resourceType = _resourceType
resourcePath = _resourcePath
getSchema()
}
export function closeDrawer() {
resourceType = undefined
resourcePath = undefined
refreshCount = 0
refreshing = false
}
let { resourceType, resourcePath }: Props = $props()
let dbSchema: DBSchema | undefined = $derived(
resourcePath in $dbSchemas ? $dbSchemas[resourcePath] : undefined
)
let isDrawerOpen: boolean = $state(false)
let shouldDisplayError = $derived(
resourcePath && resourcePath in $dbSchemas && !$dbSchemas[resourcePath]
resourcePath && resourcePath in $dbSchemas ? $dbSchemas[resourcePath] : undefined
)
// `refreshCount` is a derived state. `refreshing` is the source of truth
@@ -56,11 +57,11 @@
let expand = $state(false)
$effect(() => {
if (!isDrawerOpen) expand = false
if (!open) expand = false
})
async function getSchema() {
if ($dbSchemas[resourcePath] && !refreshing) return
if (!resourcePath || !resourceType || ($dbSchemas[resourcePath] && !refreshing)) return
try {
const oldDbSchema = $dbSchemas[resourcePath]
await getDbSchemas(
@@ -69,9 +70,7 @@
$workspaceStore,
$dbSchemas,
(message: string) => {
if (isDrawerOpen) {
sendUserToast(message, true)
}
if (open) sendUserToast(message, true)
}
)
// avoid infinite loop on error due to the way getDbSchemas is implemented
@@ -95,6 +94,8 @@
let cachedLastRefreshCount = 0
async function getColDefs(tableKey: string) {
if (!resourcePath || !resourceType) return []
if (cachedLastRefreshCount !== refreshCount) cachedColDefs = {}
cachedLastRefreshCount = refreshCount
if (cachedColDefs[tableKey]) {
@@ -130,37 +131,26 @@
}}
/>
{#if shouldDisplayError}
<Alert type="error" size="xs" title="Schema not available" class="mt-2">
Schema could not be loaded. Please check the permissions of the resource.
</Alert>
{:else}
<Button
size="xs"
variant="border"
spacingSize="xs2"
btnClasses="mt-1 w-24"
on:click={async () => {
if (!dbSchema || !$workspaceStore) refreshing = true
isDrawerOpen = true
}}
>
<Database size={18} /> Manager
</Button>
<Drawer bind:open={isDrawerOpen} size={expand ? `${windowWidth}px` : '1200px'} preventEscape>
<Drawer
bind:open
size={expand ? `${windowWidth}px` : '1200px'}
preventEscape
on:close={closeDrawer}
>
{#key [resourceType, resourcePath, dbSchema]}
<DrawerContent
title={replResultData ? 'Query Result' : 'Database Manager'}
on:close={() => {
if (replResultData) {
replResultData = undefined
} else {
isDrawerOpen = false
closeDrawer()
}
}}
CloseIcon={replResultData ? ArrowLeft : undefined}
noPadding
>
{#if dbSchema && $workspaceStore}
{#if dbSchema && $workspaceStore && resourceType && resourcePath}
<Splitpanes horizontal>
<Pane class="relative">
<!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -191,8 +181,8 @@
dbTableOpsWithPreviewScripts({
colDefs,
tableKey,
resourcePath,
resourceType,
resourcePath: resourcePath!,
resourceType: resourceType!,
workspace: $workspaceStore
})}
dbTableActionsFactory={[
@@ -204,16 +194,16 @@
]}
{refresh}
dbTableEditorPropsFactory={({ selectedSchemaKey }) => ({
resourceType,
resourceType: resourceType!,
previewSql: (values) =>
makeCreateTableQuery(values, resourceType, selectedSchemaKey),
makeCreateTableQuery(values, resourceType!, selectedSchemaKey),
async onConfirm(values) {
await runScriptAndPollResult({
workspace: $workspaceStore,
requestBody: {
args: { database: '$res:' + resourcePath },
content: makeCreateTableQuery(values, resourceType, selectedSchemaKey),
language: getLanguageByResourceType(resourceType)
content: makeCreateTableQuery(values, resourceType!, selectedSchemaKey),
language: getLanguageByResourceType(resourceType!)
}
})
refresh()
@@ -268,5 +258,5 @@
/>
{/snippet}
</DrawerContent>
</Drawer>
{/if}
{/key}
</Drawer>
@@ -6,12 +6,13 @@
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
import { Button } from './common'
import DBManagerDrawerButton from './DBManagerDrawerButton.svelte'
import { Pen, Plus, RotateCw } from 'lucide-svelte'
import { sendUserToast } from '$lib/toast'
import { isDbType } from './apps/components/display/dbtable/utils'
import Select from './select/Select.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import DbManagerDrawer from './DBManagerDrawer.svelte'
import ExploreAssetButton from '../../routes/(root)/(logged)/assets/ExploreAssetButton.svelte'
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
@@ -146,6 +147,7 @@
let appConnect: AppConnect | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
</script>
<AppConnect
@@ -253,6 +255,12 @@
/>
</div>
{#if showSchemaExplorer && isDbType(resourceType) && value}
<DBManagerDrawerButton {resourceType} resourcePath={value} />
<ExploreAssetButton
_resourceMetadata={{ resourceType }}
asset={{ kind: 'resource', path: value }}
{dbManagerDrawer}
/>
{/if}
</div>
<DbManagerDrawer bind:this={dbManagerDrawer} />
@@ -46,7 +46,7 @@
import type { ScriptOptions } from './copilot/chat/ContextManager.svelte'
import { aiChatManager, AIMode } from './copilot/chat/AIChatManager.svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
import AssetsDetectedBadge from './assets/AssetsDetectedBadge.svelte'
import AssetsDetectedBadge from './assets/AssetsDropdownButton.svelte'
interface Props {
// Exported
@@ -510,7 +510,9 @@
<Pane bind:size={codePanelSize} minSize={10} class="!overflow-visible">
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38] relative">
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
<AssetsDetectedBadge {assets} />
{#if assets.length}
<AssetsDetectedBadge {assets} />
{/if}
{#if testPanelSize === 0}
<HideButton
hidden={true}
@@ -1,37 +0,0 @@
<script lang="ts">
import { clone, pluralize } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import { Pyramid } from 'lucide-svelte'
const { assets }: { assets: string[] } = $props()
let prevAssets = $state<string[]>(assets ? clone(assets) : [])
let blueBgDiv: HTMLDivElement | undefined = $state()
$effect(() => {
if (!assets) return
if (!deepEqual(assets, prevAssets)) {
prevAssets = clone(assets)
// Replay animation
if (blueBgDiv) {
blueBgDiv.style.animation = 'none'
blueBgDiv.offsetHeight /* trigger reflow */
blueBgDiv.style.animation = ''
}
}
})
</script>
{#if assets.length > 0}
<div
class="text-xs z-50 flex text-tertiary items-center gap-2 px-2 py-1 bg-surface rounded-md border relative"
>
<div
bind:this={blueBgDiv}
class="absolute bg-blue-400/30 inset-0 rounded-md -z-50 opacity-0 animate-fade-out"
></div>
<Pyramid size={16} />
{pluralize(assets.length, 'asset')}
</div>
{/if}
@@ -0,0 +1,91 @@
<script lang="ts">
import { clone, pluralize, truncate } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import { Pyramid } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { Popover } from '../meltComponents'
import S3FilePicker from '../S3FilePicker.svelte'
import ExploreAssetButton, {
assetCanBeExplored
} from '../../../routes/(root)/(logged)/assets/ExploreAssetButton.svelte'
import { formatAsset, parseAsset } from './lib'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import { untrack } from 'svelte'
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
let { assets: assetsUris }: { assets: string[] } = $props()
const assets = $derived(assetsUris.map(parseAsset).filter((x) => !!x) ?? [])
let prevAssetsUris = $state<string[]>([])
let blueBgDiv: HTMLDivElement | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let isOpen = $state(false)
let resourceTypesCache: Record<string, string | undefined> = $state({})
$effect(() => {
if (!assetsUris) return
untrack(() => {
if (deepEqual(assetsUris, prevAssetsUris)) return
prevAssetsUris = clone(assetsUris)
// Replay animation
if (blueBgDiv) {
blueBgDiv.style.animation = 'none'
blueBgDiv.offsetHeight /* trigger reflow */
blueBgDiv.style.animation = ''
}
for (const asset of assets) {
if (asset.kind !== 'resource' || asset.path in resourceTypesCache) continue
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! })
.then((resource) => (resourceTypesCache[asset.path] = resource.resource_type))
.catch((err) => (resourceTypesCache[asset.path] = undefined))
}
})
})
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} bind:isOpen>
<svelte:fragment slot="trigger">
<div
class={twMerge(
'text-xs flex items-center gap-2 px-2 py-1.5 bg-surface rounded-md border relative',
'transition-colors hover:bg-surface-hover hover:text-primary active:bg-surface/0 cursor-pointer'
)}
>
<div
bind:this={blueBgDiv}
class="absolute pointer-events-none bg-blue-300/60 inset-0 rounded-md opacity-0 animate-fade-out"
></div>
<Pyramid size={16} class="z-10" />
<span class="z-10">
{pluralize(assets.length, 'asset')}
</span>
</div>
</svelte:fragment>
<svelte:fragment slot="content">
<ul class="divide-y rounded-md">
{#each assets as asset}
{#if asset}
<li class="text-sm px-4 h-12 flex gap-4 items-center select-none justify-between">
{truncate(formatAsset(asset), 40)}
{#if assetCanBeExplored(asset, { resourceType: resourceTypesCache[asset.path] })}
<ExploreAssetButton
{asset}
{s3FilePicker}
{dbManagerDrawer}
onClick={() => (isOpen = false)}
noText
_resourceMetadata={{ resourceType: resourceTypesCache[asset.path] }}
/>
{/if}
</li>
{/if}
{/each}
</ul>
</svelte:fragment>
</Popover>
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={dbManagerDrawer} />
+3 -5
View File
@@ -1,9 +1,7 @@
import type { AssetKind, ListAssetsResponse } from '$lib/gen'
import type { AssetKind as _AssetKind, Asset as _Asset, ListAssetsResponse } from '$lib/gen'
export type Asset = {
path: string
kind: AssetKind
}
export type Asset = _Asset
export type AssetKind = _AssetKind
export function parseAsset(asset: string): Asset | undefined {
if (asset.startsWith('$res:')) return { path: asset.substring(5), kind: 'resource' }
@@ -1,18 +1,20 @@
<script lang="ts">
import { formatAsset, getAssetUsagePageUri } from '$lib/components/assets/lib'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Button, ClearableInput, DrawerContent } from '$lib/components/common'
import { ClearableInput, DrawerContent } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import DbManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import { Cell, DataTable } from '$lib/components/table'
import Head from '$lib/components/table/Head.svelte'
import { AssetService, type ListAssetsResponse } from '$lib/gen'
import { AssetService, ResourceService, type ListAssetsResponse } from '$lib/gen'
import { userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { isS3Uri, pluralize, truncate } from '$lib/utils'
import { File } from 'lucide-svelte'
import { pluralize, truncate } from '$lib/utils'
import { untrack } from 'svelte'
import ExploreAssetButton from './ExploreAssetButton.svelte'
let assets = usePromise(() => AssetService.listAssets({ workspace: $workspaceStore ?? '' }))
@@ -23,8 +25,22 @@
) ?? []
)
let resourceTypesCache: Record<string, string | undefined> = $state({})
$effect(() => {
if (!assets.value) return
untrack(() => {
for (const asset of assets.value) {
if (asset.kind !== 'resource' || asset.path in resourceTypesCache) continue
ResourceService.getResource({ path: asset.path, workspace: $workspaceStore! })
.then((resource) => (resourceTypesCache[asset.path] = resource.resource_type))
.catch((err) => (resourceTypesCache[asset.path] = undefined))
}
})
})
let viewOccurences: ListAssetsResponse[number] | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
</script>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.resources}
@@ -59,17 +75,13 @@
</a>
</Cell>
<Cell>
{#if asset.kind === 's3object'}
<Button
size="xs"
variant="border"
spacingSize="xs2"
btnClasses="w-24"
on:click={async () => isS3Uri(assetUri) && s3FilePicker?.open(assetUri)}
>
<File size={18} /> View
</Button>
{/if}
<ExploreAssetButton
{asset}
{s3FilePicker}
{dbManagerDrawer}
_resourceMetadata={{ resourceType: resourceTypesCache[asset.path] }}
class="w-24"
/>
</Cell>
</tr>
{/each}
@@ -109,3 +121,4 @@
</DrawerContent>
</Drawer>
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={dbManagerDrawer} />
@@ -0,0 +1,61 @@
<script module lang="ts">
export function assetCanBeExplored(
asset: Asset,
_resourceMetadata?: { resourceType?: string }
): boolean {
return (
asset.kind === 's3object' ||
(asset.kind === 'resource' && isDbType(_resourceMetadata?.resourceType))
)
}
</script>
<script lang="ts">
import { isDbType } from '$lib/components/apps/components/display/dbtable/utils'
import { formatAsset, type Asset } from '$lib/components/assets/lib'
import { Button } from '$lib/components/common'
import DbManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import { isS3Uri } from '$lib/utils'
import { Database, File } from 'lucide-svelte'
const {
asset,
_resourceMetadata,
s3FilePicker,
dbManagerDrawer,
onClick,
class: className = '',
noText = false
}: {
asset: Asset
_resourceMetadata?: { resourceType?: string }
s3FilePicker?: S3FilePicker
dbManagerDrawer?: DbManagerDrawer
onClick?: () => void
class?: string
noText?: boolean
} = $props()
const assetUri = $derived(formatAsset(asset))
</script>
<Button
size="xs"
variant="border"
spacingSize="xs2"
wrapperClasses={className}
on:click={async () => {
if (asset.kind === 'resource' && isDbType(_resourceMetadata?.resourceType)) {
dbManagerDrawer?.openDrawer(_resourceMetadata.resourceType, asset.path)
} else if (asset.kind === 's3object' && isS3Uri(assetUri)) {
s3FilePicker?.open(assetUri)
}
onClick?.()
}}
>
{#if asset.kind === 's3object'}
<span class:hidden={noText}>Explore</span> <File size={18} />
{:else if asset.kind === 'resource'}
<span class:hidden={noText}>Manage</span> <Database size={18} />
{/if}
</Button>
@@ -60,8 +60,9 @@
import EditableSchemaWrapper from '$lib/components/schema/EditableSchemaWrapper.svelte'
import ResourceEditorDrawer from '$lib/components/ResourceEditorDrawer.svelte'
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
import DbManagerDrawerButton from '$lib/components/DBManagerDrawerButton.svelte'
import { isDbType } from '$lib/components/apps/components/display/dbtable/utils'
import ExploreAssetButton from '../assets/ExploreAssetButton.svelte'
import DbManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
type ResourceW = ListableResource & { canWrite: boolean; marked?: string }
type ResourceTypeW = ResourceType & { canWrite: boolean }
@@ -414,6 +415,8 @@
})
}
})
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
</script>
<ConfirmationModal
@@ -901,10 +904,11 @@
</Cell>
<Cell class="flex justify-end">
{#if path && isDbType(resource_type)}
<DbManagerDrawerButton
resourcePath={path}
resourceType={resource_type}
class="mr-8"
<ExploreAssetButton
asset={{ kind: 'resource', path: path }}
{dbManagerDrawer}
_resourceMetadata={{ resourceType: resource_type }}
class="w-24"
/>
{/if}
<Dropdown
@@ -1083,6 +1087,7 @@
<SupabaseConnect bind:this={supabaseConnect} on:refresh={loadResources} />
<AppConnect bind:this={appConnect} on:refresh={loadResources} />
<ResourceEditorDrawer bind:this={resourceEditor} on:refresh={loadResources} />
<DbManagerDrawer bind:this={dbManagerDrawer} />
<ShareModal
bind:this={shareModal}
+9 -2
View File
@@ -1,5 +1,7 @@
const plugin = require('tailwindcss/plugin')
const { tailwindClasses } = require('./src/lib/components/apps/editor/componentsPanel/tailwindUtils')
const {
tailwindClasses
} = require('./src/lib/components/apps/editor/componentsPanel/tailwindUtils')
const { zIndexes } = require('./src/lib/zIndexes')
const lightTheme = {
@@ -477,7 +479,8 @@ const config = {
},
animation: {
'spin-counter-clockwise': 'spin-counter-clockwise 1s linear infinite',
'zoom-in': 'zoom-in 0.25s ease-in-out'
'zoom-in': 'zoom-in 0.25s ease-in-out',
'fade-out': 'fade-out 1s ease-in-out'
},
keyframes: {
'spin-counter-clockwise': {
@@ -486,6 +489,10 @@ const config = {
'zoom-in': {
'0%': { transform: 'scale(0.95)' },
'100%': { transform: 'scale(1)' }
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' }
}
}
}