feat: favorite datatable and ducklake tables + asset page nits (#7795)

* update cf worker hostname

* set remote_url cookie from param

* ephemeral backends v1

* nit

* Run queue server

* ntis

* timeout

* better db process management

* commit hash and worktree

* nit use map

* nit

* err handling

* Revert "err handling"

This reverts commit 19de00c0c0.

* nits

* auto cleanup

* Ephemeral backend command action

* remove checkout

* checkout ee repo

* nits

* process.env.GIT_EE_DEPLOY_KEY_FILE

* resumeURLs logic

* nit

* use windmill flow for ephemeral backend action

* fixes

* new token

* worktree pools

* Delete GH secret on cleanup

* linux deploy

* nit

* nit

* unhandled promises

* nit

* fix docker bridge IP on linux

* pass cf_frontend_url to wmill flow

* git fetch

* release worktree when binary started

* send error

* logger

* logging

* logging 2

* delete log files periodically

* redirect to raw app with logs

* CORS

* MANAGER_AUTH_TOKEN

* Check organization membership

* nit

* bwrap

* nit

* return timeoutAt in resumeUrl

* nit

* Change password

* nit remove https

* Settings icon instead of plain text

* doc link

* favorite icon

* Better favorites state logic

* Asset favorites

* nit fixes and better label display

* global db manager drawer

* Favorites menu open db manager

* favorites in assets page + backend refactor

* Delete favorite dropdown

* parseFavoriteAsset

* git fail

* nit

* git fail

* nit

* nits

* git fail

* Table2 instead of pyramid
This commit is contained in:
Diego Imbert
2026-02-04 19:48:51 +01:00
committed by GitHub
parent 149da9b763
commit a3d75ba10a
36 changed files with 457 additions and 243 deletions
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,10 @@
-- Add up migration script here
DO
$do$
BEGIN
ALTER TYPE FAVORITE_KIND ADD VALUE 'asset';
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'Couldn''t create FAVORITE_KIND::asset: %', SQLERRM;
END
$do$;
+26 -2
View File
@@ -15018,7 +15018,7 @@ paths:
type: string
favorite_kind:
type: string
enum: [flow, app, script, raw_app]
enum: [flow, app, script, raw_app, asset]
responses:
"200":
description: star item
@@ -15041,7 +15041,7 @@ paths:
type: string
favorite_kind:
type: string
enum: [flow, app, script, raw_app]
enum: [flow, app, script, raw_app, asset]
responses:
"200":
description: unstar item
@@ -16671,6 +16671,30 @@ paths:
access_type:
$ref: "#/components/schemas/AssetUsageAccessType"
/w/{workspace}/assets/list_favorites:
get:
summary: List all favorite assets for the authenticated user
operationId: listFavoriteAssets
tags:
- asset
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: list of favorite assets
content:
application/json:
schema:
type: array
items:
type: object
required: [path]
properties:
path:
type: string
description: The asset path
/mcp/w/{workspace}/list_tools:
get:
summary: list available MCP tools
+27
View File
@@ -18,6 +18,7 @@ pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_assets))
.route("/list_by_usages", post(list_assets_by_usages))
.route("/list_favorites", get(list_favorites))
}
#[derive(Deserialize)]
@@ -286,3 +287,29 @@ async fn list_assets_by_usages(
}
Ok(Json(assets_vec))
}
async fn list_favorites(
authed: ApiAuthed,
Path(w_id): Path<String>,
Extension(user_db): Extension<UserDB>,
) -> JsonResult<Vec<Value>> {
let mut tx = user_db.begin(&authed).await?;
let favorites = sqlx::query_scalar!(
r#"SELECT
jsonb_strip_nulls(jsonb_build_object(
'path', favorite.path
)) as "favorite_asset!: _"
FROM favorite
WHERE favorite.workspace_id = $1
AND favorite.usr = $2
AND favorite_kind = 'asset'
"#,
&w_id,
&authed.username
)
.fetch_all(&mut *tx)
.await?;
Ok(Json(favorites))
}
+1
View File
@@ -31,6 +31,7 @@ pub enum FavoriteKind {
App,
#[allow(non_camel_case_types)]
Raw_App,
Asset,
}
#[derive(Deserialize)]
pub struct Favorite {
@@ -112,7 +112,6 @@
} else if (kind === 'folder') {
const folder = await FolderService.getFolder({ workspace, name: path.slice(2) })
return folder.summary
}
} catch (error) {
console.error(`Failed to fetch summary for ${kind}:${path}`, error)
@@ -122,7 +121,9 @@
async function fetchSummaries(diffs: WorkspaceItemDiff[]) {
// Only fetch summaries for scripts, flows, and apps
const itemsToFetch = diffs.filter((diff) => ['script', 'flow', 'app', 'folder'].includes(diff.kind))
const itemsToFetch = diffs.filter((diff) =>
['script', 'flow', 'app', 'folder'].includes(diff.kind)
)
for (const diff of itemsToFetch) {
const key = getItemKey(diff)
@@ -915,7 +916,6 @@
kind={diff.kind}
canFavorite={false}
workspaceId=""
starred={false}
>
{#snippet customSummary()}
{#if oldSummary != newSummary && isSelectable && existsInBothWorkspaces}
+18 -4
View File
@@ -23,6 +23,8 @@
import { resource } from 'runed'
import { capitalize, onlyAlphaNumAndUnderscore, pluralize } from '$lib/utils'
import type { DbFeatures } from './apps/components/display/dbtable/dbFeatures'
import Star from './Star.svelte'
import type { Asset } from '$lib/gen'
/** Represents a selected table with its schema */
export interface SelectedTable {
@@ -50,6 +52,7 @@
/** Tables that are already added and should show as disabled */
disabledTables?: SelectedTable[]
features?: DbFeatures
asset?: Asset
}
let {
dbType,
@@ -67,7 +70,8 @@
multiSelectMode = false,
selectedTables = $bindable([]),
disabledTables = [],
features
features,
asset
}: Props = $props()
// Helper to check if a table is selected in multi-select mode
@@ -442,16 +446,26 @@
{:else}
<!-- Normal mode: show tables for selected schema -->
{#each filteredTableKeys as tableKey}
<!-- PLACEHOLDER -->
<button
class={'w-full text-sm font-normal flex gap-2 items-center h-10 cursor-pointer pl-3 pr-1 ' +
(selected.tableKey === tableKey ? 'bg-gray-500/25' : 'hover:bg-gray-500/10')}
(selected.tableKey === tableKey ? 'bg-surface-secondary' : 'hover:bg-surface-hover')}
onclick={() => (selected.tableKey = tableKey)}
>
<Table2 class="text-primary shrink-0" size={16} />
{#if asset}
<Star
kind="asset"
path={`${asset.kind}://${asset.path == 'main' ? '' : asset.path}/${selected.schemaKey}.${tableKey}`}
/>
{:else}
<Table2 class="text-primary shrink-0" size={14} />
{/if}
<p
class="db-manager-table-key truncate text-ellipsis grow text-left text-emphasis text-xs"
>{tableKey}</p
>
{tableKey}
</p>
<DropdownV2
items={() => [
{
@@ -164,6 +164,11 @@
})}
initialTableKey={input.specificTable}
initialSchemaKey={input.type == 'database' ? input.specificSchema : undefined}
asset={_input.type == 'ducklake'
? { kind: 'ducklake', path: _input.ducklake }
: _input.resourcePath.startsWith('datatable://')
? { kind: 'datatable', path: _input.resourcePath.substring('datatable://'.length) }
: undefined}
{dbType}
refresh={() => refresh()}
{dbSelector}
@@ -57,6 +57,7 @@
)
export function openDrawer(nInput: DbInput) {
console.log('Opening DB Manager with input:', nInput)
input = nInput
if (isDatatableInput) {
datatables.refetch()
@@ -73,6 +74,8 @@
input = undefined
selectedDatatable = undefined
dbManagerContent?.clearReplResult()
if (window.location.hash.startsWith('#dbmanager:'))
history.replaceState('', document.title, window.location.href.replace(/#dbmanager:.*$/, ''))
}
let windowWidth = $state(window.innerWidth)
@@ -1,9 +1,8 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import DbManagerDrawer from './DBManagerDrawer.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
@@ -32,7 +31,7 @@
let ducklakes = usePromise(() =>
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
)
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
</script>
<div class={className}>
@@ -53,6 +52,4 @@
{dbManagerDrawer}
/>
{/if}
<DbManagerDrawer bind:this={dbManagerDrawer} />
</div>
@@ -1,6 +1,6 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { onMount, untrack } from 'svelte'
import AppConnect from './AppConnectDrawer.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
@@ -9,7 +9,6 @@
import { Loader2, Pen, Plus, RotateCw } from 'lucide-svelte'
import { sendUserToast } from '$lib/toast'
import Select from './select/Select.svelte'
import DbManagerDrawer from './DBManagerDrawer.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import DropdownV2 from './DropdownV2.svelte'
@@ -156,7 +155,7 @@
let appConnect: AppConnect | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let hovering = $state(false)
</script>
@@ -300,5 +299,3 @@
/>
{/if}
</div>
<DbManagerDrawer bind:this={dbManagerDrawer} />
+9 -25
View File
@@ -1,41 +1,25 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy'
import { FavoriteService } from '$lib/gen'
import { starStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { Star, StarOff } from 'lucide-svelte'
import { favoriteManager, type FavoriteKind } from './sidebar/FavoriteMenu.svelte'
interface Props {
path: string
kind: 'flow' | 'app' | 'script' | 'raw_app'
starred?: boolean
workspace_id: string
onStarred?: (starred: boolean) => void
kind: FavoriteKind
summary?: string
workspaceId?: string
}
let { path, kind, starred = false, workspace_id, onStarred }: Props = $props()
let { path, kind, workspaceId, summary }: Props = $props()
let buttonHover = $state(false)
let starred = $derived(favoriteManager.isStarred(path, kind))
async function onClick() {
buttonHover = false
if (starred) {
await FavoriteService.unstar({
workspace: workspace_id,
requestBody: { path, favorite_kind: kind }
})
sendUserToast('Unstarred')
$starStore = $starStore + 1
} else {
await FavoriteService.star({
workspace: workspace_id,
requestBody: { path, favorite_kind: kind }
})
sendUserToast('Marked as favorite, it will appear first')
$starStore = $starStore + 1
}
onStarred?.(!starred)
if (starred) favoriteManager.unstar(path, kind, workspaceId)
else favoriteManager.star(path, kind, workspaceId, summary)
}
</script>
@@ -43,7 +27,7 @@
onclick={preventDefault(onClick)}
onmouseenter={() => (buttonHover = true)}
onmouseleave={() => (buttonHover = false)}
class="p-2"
class="p-1"
>
{#if starred}
{#if buttonHover}
@@ -45,7 +45,7 @@
--ag-input-disabled-border-color: rgba(186, 191, 199, 0.3);
--ag-input-disabled-background-color: rgba(186, 191, 199, 0.15);
--ag-borders: solid 1px;
--ag-border-radius: 8px;
--ag-border-radius: 4px;
--ag-borders-side-button: none;
--ag-side-button-selected-background-color: transparent;
--ag-header-column-resize-handle-display: block;
@@ -66,8 +66,9 @@
--ag-widget-vertical-spacing: calc(var(--ag-grid-size) * 1.5);
--ag-toggle-button-height: 18px;
--ag-toggle-button-width: 28px;
--ag-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
Cantarell, 'Helvetica Neue', sans-serif;
--ag-font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif;
--ag-font-size: 0.7rem;
--ag-icon-font-family: agGridalpine;
--ag-selected-tab-underline-color: var(--ag-alpine-active-color);
@@ -92,8 +93,8 @@
--ag-control-panel-background-color: #222628;
--ag-subheader-background-color: #000;
--ag-input-disabled-background-color: #282c2f;
--ag-input-focus-box-shadow: 0 0 2px 0.5px rgba(255, 255, 255, 0.5),
0 0 4px 3px var(--ag-input-focus-border-color);
--ag-input-focus-box-shadow:
0 0 2px 0.5px rgba(255, 255, 255, 0.5), 0 0 4px 3px var(--ag-input-focus-border-color);
--ag-card-shadow: 0 1px 20px 1px black;
--ag-disabled-foreground-color: rgba(255, 255, 255, 0.5);
--ag-chip-background-color: rgba(255, 255, 255, 0.07);
@@ -119,8 +120,8 @@
--ag-control-panel-background-color: #222628;
--ag-subheader-background-color: #000;
--ag-input-disabled-background-color: #282c2f;
--ag-input-focus-box-shadow: 0 0 2px 0.5px rgba(255, 255, 255, 0.5),
0 0 4px 3px var(--ag-input-focus-border-color);
--ag-input-focus-box-shadow:
0 0 2px 0.5px rgba(255, 255, 255, 0.5), 0 0 4px 3px var(--ag-input-focus-border-color);
--ag-card-shadow: 0 1px 20px 1px black;
--ag-disabled-foreground-color: rgba(255, 255, 255, 0.5);
--ag-chip-background-color: rgba(255, 255, 255, 0.07);
@@ -12,10 +12,9 @@
type Asset,
type AssetWithAltAccessType
} from './lib'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import { untrack } from 'svelte'
import { ResourceService, WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import Tooltip from '../meltComponents/Tooltip.svelte'
import Tooltip2 from '../Tooltip.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
@@ -49,7 +48,7 @@
let blueBgDiv: HTMLDivElement | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
let isOpen = $state(false)
let resourceDataCache: Record<string, string | undefined> = $state({})
@@ -218,5 +217,4 @@
</svelte:fragment>
</Popover>
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={dbManagerDrawer} />
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
@@ -1,10 +1,9 @@
<script lang="ts">
import { ResourceService, type Job } from '$lib/gen'
import { inferAssets } from '$lib/infer'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { pruneNullishArray, uniqueBy } from '$lib/utils'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
import S3FilePicker from '../S3FilePicker.svelte'
import AssetButtons from './AssetButtons.svelte'
@@ -65,7 +64,7 @@
})
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
</script>
@@ -99,5 +98,4 @@
{/if}
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={dbManagerDrawer} />
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
@@ -36,7 +36,6 @@
interface Props {
app: ListableApp & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
marked: string | undefined
starred: boolean
shareModal: ShareModal
moveDrawer: MoveDrawer
deploymentDrawer: DeployWorkspaceDrawer
@@ -48,7 +47,6 @@
let {
app,
marked,
starred,
shareModal,
moveDrawer,
deploymentDrawer,
@@ -81,11 +79,6 @@
path={app.path}
summary={app.summary}
workspaceId={app.workspace_id ?? $workspaceStore ?? ''}
{starred}
onStarred={(newStarred) => {
starred = newStarred
dispatch('change')
}}
canFavorite={!app.draft_only}
{depth}
>
@@ -38,7 +38,6 @@
interface Props {
flow: Flow & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
marked: string | undefined
starred: boolean
shareModal: ShareModal
moveDrawer: MoveDrawer
deleteConfirmedCallback: (() => void) | undefined
@@ -51,7 +50,6 @@
let {
flow,
marked,
starred,
shareModal,
moveDrawer,
deleteConfirmedCallback = $bindable(),
@@ -106,12 +104,7 @@
{marked}
path={flow.path}
summary={flow.summary}
{starred}
{errorHandlerMuted}
onStarred={(newStarred) => {
starred = newStarred
dispatch('change')
}}
canFavorite={!flow.draft_only}
{depth}
>
@@ -21,7 +21,6 @@
interface Props {
app: ListableRawApp & { canWrite: boolean }
marked: string | undefined
starred: boolean
shareModal: ShareModal
moveDrawer: MoveDrawer
deleteConfirmedCallback: (() => void) | undefined
@@ -33,7 +32,6 @@
let {
app,
marked,
starred,
shareModal,
moveDrawer,
deleteConfirmedCallback = $bindable(),
@@ -75,11 +73,6 @@
path={app.path}
summary={app.summary}
workspaceId={app.workspace_id ?? $workspaceStore ?? ''}
{starred}
onStarred={(newStarred) => {
starred = newStarred
dispatch('change')
}}
canFavorite={true}
{depth}
>
@@ -8,7 +8,6 @@
interface Props {
marked: string | undefined
starred: boolean
selected?: boolean
disabled?: boolean
canFavorite?: boolean
@@ -31,12 +30,10 @@
currentTarget: EventTarget & HTMLInputElement
}
) => void
onStarred?: (starred: boolean) => void
}
let {
marked,
starred,
selected = false,
disabled = false,
canFavorite = true,
@@ -54,8 +51,7 @@
badges,
actions,
customSummary,
onSelect = () => {},
onStarred = (newStarred: boolean) => {}
onSelect = () => {}
}: Props = $props()
let displayPath: string = (depth === 0 ? path : path?.split('/')?.slice(-1)?.[0]) ?? ''
@@ -111,7 +107,7 @@
{#if canFavorite && kind !== 'resource' && kind !== 'variable' && kind !== 'resource_type'}
<div class="center-center h-full text-xs font-semibold text-secondary w-9">
<Star {kind} {path} {starred} workspace_id={workspaceId} {onStarred} />
<Star {kind} {path} {workspaceId} {summary} />
</div>
{:else}
<div class="w-9"></div>
@@ -47,7 +47,6 @@
interface Props {
script: Script & { canWrite: boolean; use_codebase: boolean }
marked: string | undefined
starred: boolean
shareModal: ShareModal
moveDrawer: MoveDrawer
deploymentDrawer: DeployWorkspaceDrawer
@@ -61,7 +60,6 @@
let {
script,
marked,
starred,
shareModal,
moveDrawer,
deploymentDrawer,
@@ -119,13 +117,8 @@
{marked}
path={script.path}
summary={script.summary}
{starred}
{errorHandlerMuted}
workspaceId={$workspaceStore ?? ''}
onStarred={(newStarred) => {
starred = newStarred
dispatch('change')
}}
canFavorite={!script.draft_only}
{depth}
>
@@ -7,7 +7,6 @@
let s = $state({
val: {
selectedAsset: undefined,
dbManagerDrawer: undefined,
s3FilePicker: undefined,
resourceEditorDrawer: undefined,
resourceMetadataCache: {},
@@ -45,7 +44,6 @@
import { deepEqual } from 'fast-equals'
import { workspaceStore } from '$lib/stores'
import S3FilePicker from '../S3FilePicker.svelte'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
import { watch } from 'runed'
@@ -176,6 +174,5 @@
{#if flowGraphAssetsCtx}
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={flowGraphAssetsCtx.val.dbManagerDrawer} />
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
{/if}
@@ -11,7 +11,6 @@ import type { StateStore } from '$lib/utils'
import type { StepsInputArgs } from './stepsInputArgs.svelte'
import type { Asset, AssetWithAccessType } from '../assets/lib'
import type S3FilePicker from '../S3FilePicker.svelte'
import type DbManagerDrawer from '../DBManagerDrawer.svelte'
import type ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
import type { ModulesTestStates } from '../modulesTest.svelte'
import type { ButtonProp } from '$lib/components/DiffEditor.svelte'
@@ -99,7 +98,6 @@ export type FlowEditorContext = {
export type FlowGraphAssetContext = StateStore<{
selectedAsset: Asset | undefined
s3FilePicker: S3FilePicker | undefined
dbManagerDrawer: DbManagerDrawer | undefined
resourceEditorDrawer: ResourceEditorDrawer | undefined
// Maps resource paths to their metadata. undefined is for error
resourceMetadataCache: Record<string, { resource_type?: string } | undefined>
@@ -234,7 +234,7 @@
import type { Edge, Node } from '@xyflow/svelte'
import { getNodeColorClasses, NODE } from '../../util'
import { userStore } from '$lib/stores'
import { globalDbManagerDrawer, userStore } from '$lib/stores'
import { deepEqual } from 'fast-equals'
import { slide } from 'svelte/transition'
import AssetColumnBadges from '$lib/components/assets/AssetColumnBadges.svelte'
@@ -311,7 +311,7 @@
noText
buttonVariant="accent"
s3FilePicker={flowGraphAssetsCtx?.val.s3FilePicker}
dbManagerDrawer={flowGraphAssetsCtx?.val.dbManagerDrawer}
dbManagerDrawer={globalDbManagerDrawer.val}
_resourceMetadata={cachedResourceMetadata}
/>
</div>
@@ -28,7 +28,6 @@
{#if item.type == 'script'}
<ScriptRow
bind:deleteConfirmedCallback
starred={item.starred ?? false}
marked={item.marked}
on:change={() => dispatch('scriptChanged')}
script={item}
@@ -46,7 +45,6 @@
{:else if item.type == 'flow'}
<FlowRow
bind:deleteConfirmedCallback
starred={item.starred ?? false}
marked={item.marked}
on:change={() => dispatch('flowChanged')}
flow={item}
@@ -63,7 +61,6 @@
{:else if item.type == 'app'}
<AppRow
bind:deleteConfirmedCallback
starred={item.starred ?? false}
marked={item.marked}
on:change={() => dispatch('appChanged')}
app={item}
@@ -76,7 +73,6 @@
{:else if item.type == 'raw_app'}
<RawAppRow
bind:deleteConfirmedCallback
starred={item.starred ?? false}
marked={item.marked}
on:change={() => dispatch('rawAppChanged')}
app={item}
@@ -57,6 +57,7 @@
{target}
onfocusin={onFocusIn}
onfocusout={onFocusOut}
onclick={onClick}
>
{@render children?.()}
</a>
@@ -1,27 +1,100 @@
<script module lang="ts">
export type FavoriteKind = Exclude<
NonNullable<StarData['requestBody']>['favorite_kind'],
undefined
>
export function getFavoriteHref(path: string, kind: FavoriteKind) {
return {
script: `/scripts/get/${path}`,
flow: `/flows/get/${path}`,
app: `/apps/get/${path}`,
raw_app: `/apps_raw/get/${path}`,
asset: `#dbmanager:${path}`
}[kind]
}
export function getFavoriteLabel(path: string, kind: FavoriteKind): string {
if (kind === 'asset') {
const asset = parseFavoriteAsset(path)
return `${asset.schema ? `${asset.schema}.` : ''}${asset.table} (${asset.assetName} ${asset.scheme})`
}
return path
}
class FavoriteManager {
current: {
label: string
path: string
href: string
kind: FavoriteKind
}[] = $state([])
async unstar(path: string, favorite_kind: FavoriteKind, workspaceId?: string) {
this.current = this.current.filter(
(fav) => !(fav.path === path && fav.kind === favorite_kind)
)
await FavoriteService.unstar({
workspace: workspaceId ?? get(workspaceStore)!,
requestBody: { path, favorite_kind }
})
}
async star(path: string, favorite_kind: FavoriteKind, workspaceId?: string, summary?: string) {
const href = getFavoriteHref(path, favorite_kind)
const label = summary || getFavoriteLabel(path, favorite_kind)
this.current = [...this.current, { href, kind: favorite_kind, label, path }]
await FavoriteService.star({
workspace: workspaceId ?? get(workspaceStore)!,
requestBody: { path, favorite_kind }
})
}
isStarred(path: string, favorite_kind: string) {
return this.current.some((fav) => fav.path === path && fav.kind === favorite_kind)
}
}
export const favoriteManager = new FavoriteManager()
export function parseFavoriteAsset(path: string) {
const [scheme, t0] = path.split('://')
const [assetName, tableKey] = t0.split('/')
const [t1, t2] = tableKey.split('.')
const [table, schema] = [
t2 ?? t1,
t1 === 'main' || t1 === 'public' ? undefined : t2 ? t1 : undefined
]
return { table, schema, assetName: assetName || 'main', path, scheme }
}
</script>
<script lang="ts">
import { CodeXml, LayoutDashboard, Star } from 'lucide-svelte'
import { CodeXml, LayoutDashboard, Table2, Star, Trash2 } from 'lucide-svelte'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import { Menu, MenuItem } from '$lib/components/meltComponents'
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
import type { MenubarBuilders } from '@melt-ui/svelte'
import { FavoriteService, type StarData } from '$lib/gen'
import { get } from 'svelte/store'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { parseDbInputFromAssetSyntax } from '$lib/utils'
import DropdownV2 from '$lib/components/DropdownV2.svelte'
interface Props {
lightMode?: boolean
isCollapsed?: boolean
favoriteLinks?: any
favoriteLinks?: {
label: string
href: string
path: string
kind: FavoriteKind
}[]
createMenu: MenubarBuilders['createMenu']
}
let {
lightMode = false,
isCollapsed = false,
favoriteLinks = [] as {
label: string
href: string
kind: 'script' | 'flow' | 'app' | 'raw_app'
}[],
createMenu
}: Props = $props()
let { lightMode = false, isCollapsed = false, favoriteLinks = [], createMenu }: Props = $props()
</script>
<Menu {createMenu} usePointerDownOutside>
@@ -46,25 +119,50 @@
</div>
{:else}
<div class="py-1 w-full max-w-full">
{#each favoriteLinks ?? [] as favorite (favorite.href)}
<MenuItem
href={favorite.href}
{item}
class="w-full inline-flex flex-row px-4 py-2 data-[highlighted]:bg-surface-hover"
>
<span class="center-center">
{#if favorite.kind == 'script'}
<CodeXml size={16} />
{:else if favorite.kind == 'flow'}
<BarsStaggered size={16} />
{:else if favorite.kind == 'app' || favorite.kind == 'raw_app'}
<LayoutDashboard size={16} />
{/if}
</span>
<span class="text-primary ml-2 grow min-w-0 text-xs truncate">
{favorite.label}
</span>
</MenuItem>
{#each favoriteLinks ?? [] as favorite}
<div class="relative group">
<MenuItem
href={favorite.href}
onClick={() => {
if (favorite.kind === 'asset') {
const dbInput = parseDbInputFromAssetSyntax(favorite.path)
if (dbInput) globalDbManagerDrawer.val?.openDrawer(dbInput)
}
}}
{item}
class="w-full inline-flex flex-row px-4 py-2 pr-8 data-[highlighted]:bg-surface-hover"
>
<span class="center-center">
{#if favorite.kind == 'script'}
<CodeXml size={16} />
{:else if favorite.kind == 'flow'}
<BarsStaggered size={16} />
{:else if favorite.kind == 'app' || favorite.kind == 'raw_app'}
<LayoutDashboard size={16} />
{:else if favorite.kind == 'asset'}
<Table2 size={16} />
{/if}
</span>
<span class="text-primary ml-2 grow min-w-0 text-xs truncate">
{favorite.label}
</span>
</MenuItem>
<div
class="absolute right-0 top-0 h-full flex items-center pr-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<DropdownV2
items={[
{
displayName: 'Delete',
icon: Trash2,
action: () => favoriteManager.unstar(favorite.path, favorite.kind)
}
]}
size="xs"
fixedHeight={false}
/>
</div>
</div>
{/each}
</div>
{/if}
@@ -12,7 +12,8 @@
Building,
Calendar,
ServerCog,
GraduationCap
GraduationCap,
Table2
} from 'lucide-svelte'
import { base } from '$lib/base'
@@ -35,21 +36,19 @@
import MenuButton, { sidebarClasses } from './MenuButton.svelte'
import MenuLink from './MenuLink.svelte'
import ResizeTransitionWrapper from '../common/ResizeTransitionWrapper.svelte'
import type { FavoriteKind } from './FavoriteMenu.svelte'
let darkMode: boolean = $state(false)
interface Props {
isCollapsed?: boolean
favoriteLinks?: any
}
let {
isCollapsed = false,
favoriteLinks = [] as {
favoriteLinks?: {
label: string
href: string
kind: 'script' | 'flow' | 'app' | 'raw_app'
kind: FavoriteKind
}[]
}: Props = $props()
}
let { isCollapsed = false, favoriteLinks = [] }: Props = $props()
let mainMenuLinks = $derived(
[
@@ -206,6 +205,8 @@
<BarsStaggered size={16} />
{:else if favorite.kind == 'app' || favorite.kind == 'raw_app'}
<LayoutDashboard size={16} />
{:else if favorite.kind == 'asset'}
<Table2 size={16} />
{/if}
</span>
<span class="text-primary ml-2 grow min-w-0 text-xs truncate">
@@ -62,12 +62,11 @@
import { random_adj } from '../random_positive_adjetive'
import { sendUserToast } from '$lib/toast'
import { SettingService, WorkspaceService, type GetSettingsResponse } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { resource } from 'runed'
import CustomInstanceDbSelect from './CustomInstanceDbSelect.svelte'
import DBManagerDrawer from '../DBManagerDrawer.svelte'
import { Popover } from '../meltComponents'
import ExploreAssetButton from '../ExploreAssetButton.svelte'
import { deepEqual } from 'fast-equals'
@@ -140,7 +139,7 @@
}
let confirmationModal = createAsyncConfirmationModal()
let dbManagerDrawer: DBManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let dirtyMap = $derived.by(() => {
const map: Record<string, boolean> = {}
for (let i = 0; i < tempSettings.dataTables.length; i++) {
@@ -290,4 +289,3 @@
<Button wrapperClasses="mt-4 mb-16 max-w-fit" on:click={onSave} variant="accent">Save</Button>
<ConfirmationModal {...confirmationModal.props} />
<DBManagerDrawer bind:this={dbManagerDrawer} />
@@ -67,10 +67,9 @@
import { SettingService, WorkspaceService } from '$lib/gen'
import type { GetSettingsResponse } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import ExploreAssetButton from '../ExploreAssetButton.svelte'
import DbManagerDrawer from '../DBManagerDrawer.svelte'
import Tooltip from '../Tooltip.svelte'
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
@@ -179,7 +178,7 @@
'Where the data is actually stored, in parquet format. You need to configure a workspace storage first'
}
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let confirmationModal = createAsyncConfirmationModal()
</script>
@@ -394,6 +393,5 @@
>
Save ducklake settings
</Button>
<DbManagerDrawer bind:this={dbManagerDrawer} />
<ConfirmationModal {...confirmationModal.props} />
+5 -1
View File
@@ -14,6 +14,7 @@ import {
import { getLocalSetting, type StateStore } from './utils'
import { createState } from './svelte5Utils.svelte'
import { DEFAULT_HUB_BASE_URL } from './hub'
import type DBManagerDrawer from './components/DBManagerDrawer.svelte'
export interface UserExt {
email: string
@@ -77,7 +78,6 @@ export const workspaceStore = writable<string | undefined>(
export const defaultScripts = writable<WorkspaceDefaultScripts | undefined>(undefined)
export const dbClockDrift = writable<number | undefined>(undefined)
export const isPremiumStore = writable<boolean>(false)
export const starStore = writable(1)
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
export const superadmin = writable<string | false | undefined>(undefined)
export const devopsRole = writable<string | false | undefined>(undefined)
@@ -127,6 +127,10 @@ export const codeCompletionSessionEnabled = writable<boolean>(
export const usedTriggerKinds = writable<string[]>([])
export let globalDbManagerDrawer: StateStore<DBManagerDrawer | undefined> = createState({
val: undefined
})
type SQLBaseSchema = {
[schemaKey: string]: {
[tableKey: string]: {
+28 -4
View File
@@ -1783,6 +1783,7 @@ import { darkModeName, lightModeName } from './assets/tokens/colorTokensConfig'
import BarsStaggered from './components/icons/BarsStaggered.svelte'
import { GitIcon } from './components/icons'
import { Bot, Code, Package } from 'lucide-svelte'
import type { DbInput } from './components/dbTypes'
export function getCssColor(
color: CssColor,
{
@@ -1986,6 +1987,23 @@ export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly
return result
}
export function parseDbInputFromAssetSyntax(path: string): DbInput | null {
const [p1, _p2] = path.split('://')
const [p2, _p3] = _p2.split('/')
const [p3, p4] = _p3.split('.')
return p1 === 'ducklake'
? { type: 'ducklake', ducklake: p2 || 'main', specificTable: p4 ?? p3 }
: p1 === 'datatable'
? {
type: 'database',
resourcePath: `datatable://${p2 || 'main'}`,
resourceType: 'postgresql',
specificTable: p4 ?? p3,
specificSchema: p4 ? p3 : undefined
}
: null
}
/**
* Formats memory size in KB to human-readable format with appropriate units
* @param sizeInKb - Memory size in kilobytes
@@ -1993,11 +2011,17 @@ export function pick<T extends object, K extends keyof T>(obj: T, keys: readonly
* @returns Formatted string with appropriate unit (KB, MB, GB) or object with display and tooltip
*/
export function formatMemory(sizeInKb: number): string
export function formatMemory(sizeInKb: number, includeTooltip: true): { display: string; tooltip: string }
export function formatMemory(sizeInKb: number, includeTooltip = false): string | { display: string; tooltip: string } {
const precise = `${sizeInKb.toLocaleString()}KB`;
export function formatMemory(
sizeInKb: number,
includeTooltip: true
): { display: string; tooltip: string }
export function formatMemory(
sizeInKb: number,
includeTooltip = false
): string | { display: string; tooltip: string } {
const precise = `${sizeInKb.toLocaleString()}KB`
let display: string;
let display: string
if (sizeInKb >= 1024 * 1024) {
// Convert to GB for values >= 1GB
display = `${(sizeInKb / (1024 * 1024)).toFixed(0)}GB`
@@ -3,6 +3,7 @@
import {
AppService,
AssetService,
FlowService,
OpenAPI,
RawAppService,
@@ -11,14 +12,19 @@
UserService,
WorkspaceService
} from '$lib/gen'
import { capitalize, classNames, getModifierKey, sendUserToast } from '$lib/utils'
import {
capitalize,
classNames,
getModifierKey,
parseDbInputFromAssetSyntax,
sendUserToast
} from '$lib/utils'
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
import CriticalAlertModal from '$lib/components/sidebar/CriticalAlertModal.svelte'
import {
enterpriseLicense,
isPremiumStore,
starStore,
superadmin,
usageStore,
workspaceUsageStore,
@@ -29,7 +35,8 @@
hubBaseUrlStore,
usedTriggerKinds,
devopsRole,
whitelabelNameStore
whitelabelNameStore,
globalDbManagerDrawer
} from '$lib/stores'
import CenteredModal from '$lib/components/CenteredModal.svelte'
import { afterNavigate, beforeNavigate } from '$app/navigation'
@@ -38,7 +45,11 @@
import SuperadminSettings from '$lib/components/SuperadminSettings.svelte'
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
import { page } from '$app/stores'
import FavoriteMenu from '$lib/components/sidebar/FavoriteMenu.svelte'
import FavoriteMenu, {
favoriteManager,
getFavoriteHref,
getFavoriteLabel
} from '$lib/components/sidebar/FavoriteMenu.svelte'
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
import { isCloudHosted } from '$lib/cloud'
import { syncTutorialsTodos } from '$lib/tutorialUtils'
@@ -54,6 +65,8 @@
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
import AiChatLayout from '$lib/components/copilot/chat/AiChatLayout.svelte'
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
import DBManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
import { watchOnce } from 'runed'
interface Props {
children?: import('svelte').Snippet
}
@@ -133,14 +146,6 @@
let innerWidth = $state(BROWSER ? window.innerWidth : 2000)
let favoriteLinks = $state(
[] as {
label: string
href: string
kind: 'app' | 'script' | 'flow' | 'raw_app'
}[]
)
function onLoad() {
loadFavorites()
loadUsage()
@@ -185,26 +190,37 @@
workspace: $workspaceStore ?? '',
starredOnly: true
})
favoriteLinks = [
const assets = await AssetService.listFavoriteAssets({ workspace: $workspaceStore ?? '' })
favoriteManager.current = [
...scripts.map((s) => ({
label: s.summary || s.path,
href: `${base}/scripts/get/${s.hash}`,
kind: 'script' as 'script'
label: s.summary || getFavoriteLabel(s.path, 'script'),
path: s.path,
href: getFavoriteHref(s.path, 'script'),
kind: 'script' as const
})),
...flows.map((f) => ({
label: f.summary || f.path,
href: `${base}/flows/get/${f.path}`,
kind: 'flow' as 'flow'
label: f.summary || getFavoriteLabel(f.path, 'flow'),
path: f.path,
href: getFavoriteHref(f.path, 'flow'),
kind: 'flow' as const
})),
...apps.map((f) => ({
label: f.summary || f.path,
href: `${base}/apps/get/${f.path}`,
kind: 'app' as 'app'
label: f.summary || getFavoriteLabel(f.path, 'app'),
path: f.path,
href: getFavoriteHref(f.path, 'app'),
kind: 'app' as const
})),
...raw_apps.map((f) => ({
label: f.summary || f.path,
href: `${base}/apps/get_raw/${f.version}/${f.path}`,
kind: 'raw_app' as 'raw_app'
label: f.summary || getFavoriteLabel(f.path, 'raw_app'),
path: f.path,
href: getFavoriteHref(f.path, 'raw_app'),
kind: 'raw_app' as const
})),
...assets.map((a) => ({
label: getFavoriteLabel(a.path, 'asset'),
path: a.path,
href: getFavoriteHref(a.path, 'asset'),
kind: 'asset' as const
}))
]
}
@@ -369,7 +385,7 @@
untrack(() => updateUserStore($workspaceStore))
})
$effect(() => {
$workspaceStore && $starStore && untrack(() => onLoad())
$workspaceStore && untrack(() => onLoad())
})
$effect(() => {
innerWidth && untrack(() => changeCollapsed())
@@ -409,6 +425,19 @@
}
}
})
watchOnce(
() => globalDbManagerDrawer.val,
() => {
if (!globalDbManagerDrawer.val) return
const hash = window.location.hash
if (hash.startsWith('#dbmanager:')) {
const [_, path] = hash.split('#dbmanager:')
const dbInput = parseDbInputFromAssetSyntax(path)
if (dbInput) globalDbManagerDrawer.val?.openDrawer(dbInput)
}
}
)
</script>
<svelte:window bind:innerWidth />
@@ -496,7 +525,7 @@
<Menubar>
{#snippet children({ createMenu })}
<WorkspaceMenu {createMenu} />
<FavoriteMenu {createMenu} {favoriteLinks} />
<FavoriteMenu {createMenu} favoriteLinks={favoriteManager.current} />
{/snippet}
</Menubar>
<MenuButton
@@ -570,7 +599,11 @@
<Menubar class="flex flex-col gap-1">
{#snippet children({ createMenu })}
<WorkspaceMenu {createMenu} {isCollapsed} />
<FavoriteMenu {createMenu} {favoriteLinks} {isCollapsed} />
<FavoriteMenu
{createMenu}
favoriteLinks={favoriteManager.current}
{isCollapsed}
/>
{/snippet}
</Menubar>
<MenuButton
@@ -624,7 +657,7 @@
{/if}
{:else}
<div class="absolute top-2 left-2 z5000">
<OperatorMenu {favoriteLinks} />
<OperatorMenu favoriteLinks={favoriteManager.current} />
</div>
{/if}
<!-- Legacy menu -->
@@ -683,7 +716,7 @@
<Menubar>
{#snippet children({ createMenu })}
<WorkspaceMenu {createMenu} />
<FavoriteMenu {createMenu} {favoriteLinks} />
<FavoriteMenu {createMenu} favoriteLinks={favoriteManager.current} />
{/snippet}
</Menubar>
<MenuButton
@@ -735,3 +768,7 @@
{:else}
<CenteredModal title="Loading user..." loading={true}></CenteredModal>
{/if}
{#if $workspaceStore}
<DBManagerDrawer bind:this={globalDbManagerDrawer.val} />
{/if}
@@ -1,7 +1,6 @@
<script lang="ts">
import { formatAsset, formatAssetKind } from '$lib/components/assets/lib'
import CenteredPage from '$lib/components/CenteredPage.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'
@@ -13,15 +12,15 @@
type AssetKind,
type ListAssetsResponse
} from '$lib/gen'
import { userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import { pluralize, truncate } from '$lib/utils'
import { userStore, workspaceStore, userWorkspaces, globalDbManagerDrawer } from '$lib/stores'
import { parseDbInputFromAssetSyntax, pluralize, truncate } from '$lib/utils'
import ExploreAssetButton, {
assetCanBeExplored
} from '../../../../lib/components/ExploreAssetButton.svelte'
import AssetsUsageDrawer from '$lib/components/assets/AssetsUsageDrawer.svelte'
import AssetGenericIcon from '$lib/components/icons/AssetGenericIcon.svelte'
import { Tooltip } from '$lib/components/meltComponents'
import { AlertTriangle, Loader2, SettingsIcon } from 'lucide-svelte'
import { AlertTriangle, Loader2, SettingsIcon, StarIcon } from 'lucide-svelte'
import { StaleWhileLoading, useInfiniteQuery, useScrollToBottom } from '$lib/svelte5Utils.svelte'
import { Debounced, resource, watch, type ResourceReturn } from 'runed'
import Label from '$lib/components/Label.svelte'
@@ -30,6 +29,7 @@
import RefreshButton from '$lib/components/common/button/RefreshButton.svelte'
import Section from '$lib/components/Section.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { favoriteManager, parseFavoriteAsset } from '$lib/components/sidebar/FavoriteMenu.svelte'
interface AssetCursor {
created_at?: string
@@ -82,7 +82,7 @@
let assets = $derived(_assets.current?.flatMap((page) => page.assets))
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let assetsUsageDropdown: AssetsUsageDrawer | undefined = $state()
let allS3Storages = resource(
@@ -101,16 +101,22 @@
() => $workspaceStore,
() =>
WorkspaceService.listDucklakes({ workspace: $workspaceStore! }).then((d) =>
d.map((d) => ({ label: d == 'main' ? 'Primary ducklake' : d, value: d }))
d.map((d) => ({ label: d == 'main' ? 'Main ducklake' : d, value: d }))
)
)
let allDataTables = resource(
() => $workspaceStore,
() =>
WorkspaceService.listDataTables({ workspace: $workspaceStore! }).then((d) =>
d.map((d) => ({ label: d == 'main' ? 'Primary data table' : d, value: d }))
d.map((d) => ({ label: d == 'main' ? 'Main data table' : d, value: d }))
)
)
function extractFavorites(kind: AssetKind) {
return favoriteManager.current
.filter((f) => f.kind === 'asset' && f.path.startsWith(kind))
.map((f) => parseFavoriteAsset(f.path))
}
</script>
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.assets}
@@ -133,14 +139,36 @@
assetKind: AssetKind
data: ResourceReturn<{ label: string; value: string }[]>
settingsHref: string
docsHref: string
favorites?: { table: string; schema?: string; assetName: string; path: string }[]
})}
<div class="flex flex-col bg-surface-tertiary drop-shadow-base rounded-md flex-1">
<div class="flex justify-between px-6 pt-5 border-b">
<h3 class="text-sm font-bold mb-4">{props.title}</h3>
<a class="text-xs" href={props.settingsHref}>Settings </a>
<div class="flex justify-between border-b">
<h3 class="text-sm font-bold mb-4 pt-5 pl-6">{props.title}</h3>
<div class="flex items-center h-fit gap-2 mt-4 mr-4">
<Button
wrapperClasses="h-fit"
btnClasses="text-accent"
variant="subtle"
unifiedSize="sm"
href={props.docsHref}
target="_blank"
>
See documentation
</Button>
<Button
wrapperClasses="h-fit"
variant={props.data.current?.length === 0 && !props.data.loading
? 'accent'
: 'subtle'}
iconOnly
endIcon={{ icon: SettingsIcon }}
href={props.settingsHref}
/>
</div>
</div>
{#if props.data.current?.length}
<div class="max-h-96 overflow-y-auto pb-3">
<div class="max-h-96 overflow-y-auto pb-1">
{#each props.data.current ?? [] as item}
<div class="text-xs py-2 text-primary flex justify-between items-center px-6">
{item.label}
@@ -153,6 +181,34 @@
</div>
{/each}
</div>
{#if !props.data.loading && !props.data.error && props.favorites != undefined}
<div class="mb-4 pt-2 px-6">
<h3 class="text-xs font-bold mb-1"> Favorite tables</h3>
<div class="flex gap-1 flex-wrap">
{#each props.favorites as fav}
<button
class="text-xs font-normal bg-surface-sunken rounded-md px-2 py-1 cursor-pointer hover:opacity-80"
onclick={() => {
const dbInput = parseDbInputFromAssetSyntax(fav.path)
if (dbInput) globalDbManagerDrawer.val?.openDrawer(dbInput)
}}
>
<span>
<StarIcon size="12" class="inline mr-1" />
{fav.schema ? `${fav.schema}.` : ''}{fav.table}
</span>
<span class="text-2xs">
{fav.assetName === 'main' ? `` : `(${fav.assetName})`}
</span>
</button>
{/each}
</div>
{#if props.favorites.length === 0}
<div class="text-xs text-secondary"> No favorite table yet</div>
{/if}
</div>
{/if}
{/if}
{#if props.data.loading}
<div class="flex items-center gap-2 mt-2 mb-5 px-6 text-sm text-secondary">
@@ -163,37 +219,35 @@
Error loading {props.title.toLowerCase()}
</div>
{:else if props.data.current?.length === 0}
<div class="text-sm text-secondary mt-2 px-6 mb-3">
<div class="text-xs text-secondary mt-2 px-6 mb-3">
No {props.title.toLowerCase()} yet
</div>
<Button
endIcon={{ icon: SettingsIcon }}
href={props.settingsHref}
variant="accent"
wrapperClasses="w-fit px-6"
>
{props.title} settings
</Button>
{/if}
</div>
{/snippet}
{@render card({
title: 'Data tables',
title: 'Data table',
data: allDataTables,
assetKind: 'datatable',
settingsHref: '/workspace_settings?tab=windmill_data_tables'
settingsHref: '/workspace_settings?tab=windmill_data_tables',
docsHref: 'https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables',
favorites: extractFavorites('datatable')
})}
{@render card({
title: 'Ducklakes',
title: 'Ducklake',
data: allDucklakes,
assetKind: 'ducklake',
settingsHref: '/workspace_settings?tab=windmill_lfs'
settingsHref: '/workspace_settings?tab=windmill_lfs',
docsHref: 'https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake',
favorites: extractFavorites('ducklake')
})}
{@render card({
title: 'Workspace object storages',
title: 'Object storage',
data: allS3Storages,
assetKind: 's3object',
settingsHref: '/workspace_settings?tab=windmill_lfs'
settingsHref: '/workspace_settings?tab=windmill_lfs',
docsHref:
'https://www.windmill.dev/docs/core_concepts/persistent_storage/large_data_files'
})}
</div>
</Section>
@@ -233,7 +287,6 @@
<AssetsUsageDrawer bind:this={assetsUsageDropdown} />
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
<DbManagerDrawer bind:this={dbManagerDrawer} />
{#snippet table()}
<DataTable>
@@ -120,8 +120,6 @@
goto('/')
}
let starred: boolean | undefined = $state(undefined)
async function loadTriggersCount() {
$triggersCount = await FlowService.getTriggersCountOfFlow({
workspace: $workspaceStore!,
@@ -146,7 +144,6 @@
path,
withStarredInfo: true
})
starred = flow.starred
if (!flow.path.startsWith(`u/${$userStore?.username}`) && flow.path.split('/').length > 2) {
invisible_to_owner = flow.visible_to_runner_only
}
@@ -512,15 +509,7 @@
/>
{/snippet}
{#if $workspaceStore && flow}
<Star
kind="flow"
path={flow.path}
{starred}
workspace_id={$workspaceStore}
onStarred={(newStarred) => {
starred = newStarred
}}
/>
<Star kind="flow" path={flow.path} summary={flow.summary} />
{/if}
{#if flow?.value?.priority != undefined}
<div class="hidden md:block">
@@ -29,7 +29,13 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import type { ResourceType, WorkspaceDeployUISettings } from '$lib/gen'
import { OauthService, ResourceService, WorkspaceService, type ListableResource } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore, userWorkspaces } from '$lib/stores'
import {
enterpriseLicense,
userStore,
workspaceStore,
userWorkspaces,
globalDbManagerDrawer
} from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import {
canWrite,
@@ -65,7 +71,6 @@
import ExploreAssetButton, {
assetCanBeExplored
} from '../../../../lib/components/ExploreAssetButton.svelte'
import DbManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
type ResourceW = ListableResource & { canWrite: boolean; marked?: string }
type ResourceTypeW = ResourceType & { canWrite: boolean }
@@ -473,7 +478,7 @@
}
})
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let filterUserFolders = $state(false)
let filterUserFoldersType: 'only f/*' | 'u/username and f/*' | undefined = $derived(
@@ -1170,7 +1175,6 @@
<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}
@@ -168,7 +168,6 @@
}
}
}
let starred: boolean | undefined = $state(undefined)
async function loadTriggers(path: string): Promise<void> {
await triggersState.fetchTriggers(
@@ -208,7 +207,6 @@
no_main_func: false,
has_preprocessor: false
}
starred = false
can_write = false
return
} catch (e) {
@@ -225,7 +223,6 @@
withStarredInfo: true,
authed: true
})
starred = script.starred
} catch {
try {
script = await ScriptService.getScriptByPath({
@@ -233,7 +230,6 @@
path: hash,
withStarredInfo: true
})
starred = script.starred
hash = script.hash
} catch (e) {
sendUserToast('Could not load script: ' + e.body, true)
@@ -651,15 +647,7 @@
/>
{/snippet}
{#if $workspaceStore && script}
<Star
kind="script"
path={script.path}
{starred}
workspace_id={$workspaceStore}
onStarred={(newStarred) => {
starred = newStarred
}}
/>
<Star kind="script" path={script.path} summary={script.summary} />
{/if}
{#if script?.codebase}
<Badge