mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
feat(frontend): App bar as components (#4103)
* feat(frontend): wip * feat(frontend): add migration to the new top bar * feat(frontend): fix topbar styling * feat(frontend): fix migration code * feat(frontend): fix migration code * feat(frontend): fix migration code * feat(frontend): wip * feat(frontend): done * feat(frontend): done * feat(frontend): remove migration * feat(frontend): fix top bar styling * feat(frontend): fix sync issues * feat(frontend): improve style * feat(frontend): Redesign Recompute all * feat(frontend): change icon + make the default title font bigger * feat(frontend): Remove unecesary clearInterval * feat(frontend): Fix dropdown menu button style
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import {
|
||||
type AppViewerContext,
|
||||
type ComponentCustomCSS,
|
||||
type RichConfigurations
|
||||
} from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import { components } from '../../editor/component'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import RecomputeAllWrapper from '../../editor/RecomputeAllWrapper.svelte'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let initializing: boolean | undefined = false
|
||||
export let customCss: ComponentCustomCSS<'jobiddisplaycomponent'> | undefined = undefined
|
||||
export let configuration: RichConfigurations
|
||||
export let render: boolean
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
|
||||
const { app, worldStore, policy } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
components['recomputeallcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
initOutput($worldStore, id, {
|
||||
loading: undefined
|
||||
})
|
||||
|
||||
initializing = false
|
||||
|
||||
let css = initCss($app.css?.recomputeallcomponent, customCss)
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['recomputeallcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#each Object.keys(css ?? {}) as key (key)}
|
||||
<ResolveStyle
|
||||
{id}
|
||||
{customCss}
|
||||
{key}
|
||||
bind:css={css[key]}
|
||||
componentStyle={$app.css?.recomputeallcomponent}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<AlignWrapper {horizontalAlignment}>
|
||||
{#if render && policy}
|
||||
<RecomputeAllWrapper
|
||||
containerClass={css?.container?.class}
|
||||
containerStyle={css?.container?.style}
|
||||
/>
|
||||
{/if}
|
||||
</AlignWrapper>
|
||||
@@ -204,7 +204,9 @@
|
||||
<div
|
||||
class="text-ternary bg-surface-primary flex justify-center items-center h-full w-full"
|
||||
>
|
||||
No text
|
||||
{#if resolvedConfig?.disableNoText === false}
|
||||
No text
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -230,11 +232,11 @@
|
||||
style={css?.text?.style}
|
||||
>
|
||||
{String(result)}
|
||||
{#if resolvedConfig.tooltip && resolvedConfig.tooltip != ''}
|
||||
<Tooltip>{resolvedConfig.tooltip}</Tooltip>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
|
||||
{#if resolvedConfig.tooltip && resolvedConfig.tooltip != ''}
|
||||
<Tooltip>{resolvedConfig.tooltip}</Tooltip>
|
||||
{/if}
|
||||
{#if resolvedConfig.copyButton && result}
|
||||
<div class="flex">
|
||||
<Button
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
|
||||
const appStore = writable<App>(app)
|
||||
const selectedComponent = writable<string[] | undefined>(undefined)
|
||||
|
||||
$: selectedComponent.subscribe((s) => {
|
||||
console.log('selectedComponent', s)
|
||||
})
|
||||
const mode = writable<EditorMode>('dnd')
|
||||
const breakpoint = writable<EditorBreakpoint>('lg')
|
||||
const summaryStore = writable(summary)
|
||||
@@ -88,6 +92,10 @@
|
||||
hoveredComponent: undefined
|
||||
})
|
||||
|
||||
summaryStore.subscribe((s) => {
|
||||
$worldStore?.outputsById['ctx'].summary.set(s)
|
||||
})
|
||||
|
||||
const cssEditorOpen = writable<boolean>(false)
|
||||
|
||||
const history = initHistory(app)
|
||||
@@ -114,7 +122,9 @@
|
||||
query: Object.fromEntries($page.url.searchParams.entries()),
|
||||
hash: $page.url.hash,
|
||||
workspace: $workspaceStore,
|
||||
mode: 'editor'
|
||||
mode: 'editor',
|
||||
summary: $summaryStore,
|
||||
author: policy.on_behalf_of_email
|
||||
}
|
||||
const darkMode: Writable<boolean> = writable(document.documentElement.classList.contains('dark'))
|
||||
|
||||
@@ -157,7 +167,8 @@
|
||||
cssEditorOpen,
|
||||
previewTheme,
|
||||
debuggingComponents: writable({}),
|
||||
replaceStateFn: (path) => replaceState(path, $page.state)
|
||||
replaceStateFn: (path) => replaceState(path, $page.state),
|
||||
policy: policy
|
||||
})
|
||||
|
||||
let scale = writable(100)
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
EditorBreakpoint,
|
||||
EditorMode
|
||||
} from '../types'
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import Button from '../../common/button/Button.svelte'
|
||||
import { Unlock } from 'lucide-svelte'
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
import GridViewer from './GridViewer.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -25,6 +23,7 @@
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { getTheme } from './componentsPanel/themeUtils'
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
|
||||
export let app: App
|
||||
export let appPath: string = ''
|
||||
@@ -37,6 +36,7 @@
|
||||
export let noBackend: boolean = false
|
||||
export let isLocked = false
|
||||
export let hideRefreshBar = false
|
||||
|
||||
export let replaceStateFn: (path: string) => void = (path: string) =>
|
||||
window.history.replaceState(null, '', path)
|
||||
export let gotoFn: (path: string, opt?: Record<string, any> | undefined) => void = (
|
||||
@@ -58,7 +58,13 @@
|
||||
|
||||
const allIdsInPath = writable<string[]>([])
|
||||
|
||||
let ncontext: any = { ...context, workspace, mode: 'viewer' }
|
||||
let ncontext: any = {
|
||||
...context,
|
||||
workspace,
|
||||
mode: 'viewer',
|
||||
summary: summary,
|
||||
author: policy.on_behalf_of_email
|
||||
}
|
||||
|
||||
function hashchange(e: HashChangeEvent) {
|
||||
ncontext.hash = e.newURL.split('#')[1]
|
||||
@@ -111,7 +117,8 @@
|
||||
previewTheme: writable(undefined),
|
||||
debuggingComponents: writable({}),
|
||||
replaceStateFn,
|
||||
gotoFn
|
||||
gotoFn,
|
||||
policy
|
||||
})
|
||||
|
||||
let previousSelectedIds: string[] | undefined = undefined
|
||||
@@ -183,7 +190,7 @@
|
||||
|
||||
<svelte:window on:hashchange={hashchange} on:resize={resizeWindow} />
|
||||
|
||||
<div class="relative min-h-screen h-full" bind:clientHeight={appHeight}>
|
||||
<div class="relative h-full" bind:clientHeight={appHeight}>
|
||||
<div id="app-editor-top-level-drawer" />
|
||||
<div id="app-editor-select" />
|
||||
|
||||
@@ -193,9 +200,9 @@
|
||||
: 'max-w-7xl'} mx-auto"
|
||||
id="app-content"
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
{#if $appStore.grid && $appStore.hideLegacyTopBar !== true}
|
||||
<div
|
||||
class={classNames(
|
||||
class={twMerge(
|
||||
'mx-auto',
|
||||
hideRefreshBar || $appStore?.norefreshbar ? 'invisible h-0 overflow-hidden' : ''
|
||||
)}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { push } from '$lib/history'
|
||||
@@ -14,12 +12,14 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import ComponentWrapper from './component/ComponentWrapper.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { BG_PREFIX } from '../utils'
|
||||
import GridEditorMenu from './GridEditorMenu.svelte'
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { BG_PREFIX } from '../utils'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import GridEditorMenu from './GridEditorMenu.svelte'
|
||||
import type { Policy } from '$lib/gen'
|
||||
|
||||
export let policy: Policy
|
||||
|
||||
@@ -48,52 +48,53 @@
|
||||
</script>
|
||||
|
||||
<div class="w-full z-[1000] overflow-visible h-full">
|
||||
<div
|
||||
class="w-full sticky top-0 flex justify-between border-b {$componentActive
|
||||
? 'invisible'
|
||||
: 'z-50'} {$connectingInput?.opened ? '' : 'bg-surface'} px-4 py-1 items-center gap-4"
|
||||
>
|
||||
<h3 class="truncate">{$summary}</h3>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
{#if !$connectingInput.opened}
|
||||
<RecomputeAllComponents />
|
||||
{#if $app.hideLegacyTopBar !== true}
|
||||
<div
|
||||
class="w-full sticky top-0 flex justify-between border-b {$componentActive
|
||||
? 'invisible'
|
||||
: 'z-50'} {$connectingInput?.opened ? '' : 'bg-surface'} px-4 py-1 items-center gap-4"
|
||||
>
|
||||
<h3 class="truncate">{$summary}</h3>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
{#if !$connectingInput.opened}
|
||||
<RecomputeAllComponents />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $bgRuns.length > 0}
|
||||
<Popover notClickable>
|
||||
<span class="!text-2xs text-tertiary inline-flex gap-1 items-center"
|
||||
><Loader2 size={10} class="animate-spin" /> {$bgRuns.length}
|
||||
</span>
|
||||
<span slot="text"
|
||||
><div class="flex flex-col">
|
||||
{#each $bgRuns as bgRun}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-2xs text-tertiary">{bgRun}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div></span
|
||||
>
|
||||
</Popover>
|
||||
{:else}
|
||||
<span class="w-9" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $bgRuns.length > 0}
|
||||
<Popover notClickable>
|
||||
<span class="!text-2xs text-tertiary inline-flex gap-1 items-center"
|
||||
><Loader2 size={10} class="animate-spin" /> {$bgRuns.length}
|
||||
</span>
|
||||
<span slot="text"
|
||||
><div class="flex flex-col">
|
||||
{#each $bgRuns as bgRun}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-2xs text-tertiary">{bgRun}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div></span
|
||||
>
|
||||
</Popover>
|
||||
{:else}
|
||||
<span class="w-9" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex text-2xs gap-8 items-center">
|
||||
<div class="py-2 pr-2 text-secondary flex gap-1 items-center">
|
||||
Hide bar on view
|
||||
<Toggle size="xs" bind:checked={$app.norefreshbar} />
|
||||
</div>
|
||||
<div>
|
||||
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''}
|
||||
<Tooltip>
|
||||
The scripts will be run on behalf of the author and a tight policy ensure security about
|
||||
the possible inputs of the runnables.
|
||||
</Tooltip>
|
||||
<div class="flex text-2xs gap-8 items-center">
|
||||
<div class="py-2 pr-2 text-secondary flex gap-1 items-center">
|
||||
Hide bar on view
|
||||
<Toggle size="xs" bind:checked={$app.norefreshbar} />
|
||||
</div>
|
||||
<div>
|
||||
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''}
|
||||
<Tooltip>
|
||||
The scripts will be run on behalf of the author and a tight policy ensure security about
|
||||
the possible inputs of the runnables.
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
style={$app.css?.['app']?.['grid']?.style}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script lang="ts" context="module">
|
||||
let loading: Writable<boolean> = writable(false)
|
||||
let progress: Writable<number> = writable(100)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { RefreshCw } from 'lucide-svelte'
|
||||
import { Loader2, RefreshCw, TimerReset } from 'lucide-svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import Button from '../../common/button/Button.svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
@@ -7,15 +12,18 @@
|
||||
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
|
||||
const { runnableComponents, app, initialized } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const appEditorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let loading: boolean = false
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
let interval: number | undefined = undefined
|
||||
let shouldRefresh = false
|
||||
let firstLoad = false
|
||||
let progressTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
$: !firstLoad &&
|
||||
$initialized.initializedComponents?.length ==
|
||||
@@ -32,23 +40,43 @@
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', visChange)
|
||||
if (timeout) clearInterval(timeout)
|
||||
if (progressTimer) clearInterval(progressTimer)
|
||||
}
|
||||
})
|
||||
|
||||
function onClick(stopAfterClear = true) {
|
||||
function onClick(stopAfterClear = false) {
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
timeout = undefined
|
||||
shouldRefresh = false
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer)
|
||||
progressTimer = undefined
|
||||
}
|
||||
if (stopAfterClear) return
|
||||
}
|
||||
refresh()
|
||||
if (interval) {
|
||||
shouldRefresh = true
|
||||
timeout = setInterval(refresh, interval)
|
||||
startProgress()
|
||||
}
|
||||
}
|
||||
|
||||
function startProgress() {
|
||||
progress.set(100)
|
||||
if (progressTimer) clearInterval(progressTimer)
|
||||
progressTimer = setInterval(() => {
|
||||
progress.update((n) => {
|
||||
const newProgress = n - 100 / ((interval ?? 1000) / 100)
|
||||
if (newProgress <= 0) {
|
||||
return 0
|
||||
}
|
||||
return newProgress
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function setInter(inter: number | undefined) {
|
||||
interval = inter
|
||||
onClick(!inter)
|
||||
@@ -62,7 +90,8 @@
|
||||
firstLoad = true
|
||||
isFirstLoad = true
|
||||
}
|
||||
loading = true
|
||||
$loading = true
|
||||
$progress = 100
|
||||
|
||||
console.log('refresh all')
|
||||
refreshing = []
|
||||
@@ -95,7 +124,7 @@
|
||||
.filter(Boolean)
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
loading = false
|
||||
$loading = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,9 +133,11 @@
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
timeout = undefined
|
||||
if (progressTimer) clearInterval(progressTimer)
|
||||
}
|
||||
} else if (shouldRefresh) {
|
||||
timeout = setInterval(refresh, interval)
|
||||
startProgress()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,53 +163,86 @@
|
||||
.join(', ')} -->
|
||||
<!-- {allItems($app.grid, $app.subgrids).map((x) => x.id)} -->
|
||||
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
disabled={componentNumber == 0}
|
||||
on:click={() => onClick()}
|
||||
color={timeout ? 'blue' : 'light'}
|
||||
variant={timeout ? 'contained' : 'border'}
|
||||
size="xs"
|
||||
btnClasses="!rounded-r-none text-tertiary !text-2xs {timeout ? '!border !border-blue-500' : ''}"
|
||||
title="Refresh {componentNumber} component{componentNumber > 1 ? 's' : ''} {interval
|
||||
? `every ${interval / 1000} seconds`
|
||||
: 'once'} {refreshing.length > 0 ? `(live: ${refreshing.join(', ')}))` : ''}"
|
||||
>
|
||||
<RefreshCw class={loading ? 'animate-spin' : ''} size={14} /> {componentNumber}
|
||||
</Button>
|
||||
<div class=" border rounded-md overflow-hidden">
|
||||
<div class={twMerge('flex items-center')}>
|
||||
<Button
|
||||
disabled={componentNumber == 0}
|
||||
on:click={() => onClick()}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
btnClasses={twMerge(
|
||||
'!rounded-none text-tertiary !text-2xs !border-r border-y-0 border-l-0 group'
|
||||
)}
|
||||
title="Refresh {componentNumber} component{componentNumber > 1 ? 's' : ''} {interval
|
||||
? `every ${interval / 1000} seconds`
|
||||
: 'Once'} {refreshing.length > 0 ? `(live: ${refreshing.join(', ')}))` : ''}"
|
||||
>
|
||||
<div class="z-10 flex flex-row items-center gap-2">
|
||||
{#if !$loading}
|
||||
<RefreshCw size={14} />
|
||||
{:else}
|
||||
<Loader2 class="animate-spin text-blue-500" size={14} />
|
||||
{/if}
|
||||
|
||||
<ButtonDropdown hasPadding={true}>
|
||||
<svelte:fragment slot="label">
|
||||
<span
|
||||
class={classNames('text-xs min-w-[2rem]', interval ? 'text-blue-500' : 'text-tertiary')}
|
||||
>
|
||||
{interval ? `${interval / 1000}s` : 'once'}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
{#each items ?? [] as { }, index}
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
if (index === 0) {
|
||||
setInter(undefined)
|
||||
} else {
|
||||
setInter(index * 5000)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'!text-tertiary text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
{#if index === 0}
|
||||
Once
|
||||
{:else}
|
||||
{`Every ${index * 5} seconds`}
|
||||
{/if}
|
||||
({componentNumber})
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<slot:fragment slot="buttonReplacement">
|
||||
<div class="flex flex-row gap-2 text-xs hover:bg-surface-hover px-2 items-center h-7">
|
||||
{#if interval}
|
||||
<Badge color="blue" small>
|
||||
{interval ? `Every ${interval / 1000}s` : 'Once'}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center items-center">
|
||||
<TimerReset size={14} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
</div>
|
||||
</slot:fragment>
|
||||
<svelte:fragment slot="label">
|
||||
<span
|
||||
class={twMerge('text-xs min-w-[2rem] ', interval ? 'text-blue-500' : 'text-tertiary')}
|
||||
>
|
||||
{interval ? `${interval / 1000}s` : 'Once'}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
{#each items ?? [] as { }, index}
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
if (index === 0) {
|
||||
setInter(undefined)
|
||||
} else {
|
||||
setInter(index * 5000)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'!text-tertiary text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
{#if index === 0}
|
||||
Once
|
||||
{:else}
|
||||
{`Every ${index * 5} seconds`}
|
||||
{/if}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
</div>
|
||||
{#if interval}
|
||||
<div class="w-full bg-gray-200 rounded-full h-0.5 dark:bg-gray-700">
|
||||
<div
|
||||
class="bg-blue-300 h-0.5 rounded-full dark:bg-blue-500 transition-all"
|
||||
style="width: {$progress}%"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
|
||||
import { dfs } from './appUtils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let containerClass: string | undefined = undefined
|
||||
export let containerStyle: string | undefined = undefined
|
||||
|
||||
const { selectedComponent, app, connectingInput, allIdsInPath, bgRuns } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let previousSelectedIds: string[] | undefined = undefined
|
||||
$: if (!deepEqual(previousSelectedIds, $selectedComponent)) {
|
||||
previousSelectedIds = $selectedComponent
|
||||
$allIdsInPath = ($selectedComponent ?? [])
|
||||
.flatMap((id) => dfs($app.grid, id, $app.subgrids ?? {}))
|
||||
.filter((x) => x != undefined) as string[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge('flex justify-center h-8 items-center gap-2', containerClass)}
|
||||
style={containerStyle}
|
||||
>
|
||||
<div class="w-9">
|
||||
{#if $bgRuns.length > 0}
|
||||
<Popover notClickable>
|
||||
<span class="!text-2xs text-tertiary inline-flex gap-1 items-center"
|
||||
><Loader2 size={10} class="animate-spin" /> {$bgRuns.length}
|
||||
</span>
|
||||
<span slot="text"
|
||||
><div class="flex flex-col">
|
||||
{#each $bgRuns as bgRun}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="text-2xs">{bgRun}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div></span
|
||||
>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if !$connectingInput.opened}
|
||||
<RecomputeAllComponents />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
App,
|
||||
BaseAppComponent,
|
||||
ComponentCustomCSS,
|
||||
ConnectingInput,
|
||||
EditorBreakpoint,
|
||||
FocusedGrid,
|
||||
@@ -20,7 +21,7 @@ import { gridColumns } from '../gridUtils'
|
||||
import { allItems } from '../utils'
|
||||
import type { Output, World } from '../rx'
|
||||
import gridHelp from '../svelte-grid/utils/helper'
|
||||
import type { FilledItem } from '../svelte-grid/types'
|
||||
import type { FilledItem, Size } from '../svelte-grid/types'
|
||||
import type {
|
||||
StaticAppInput,
|
||||
EvalAppInput,
|
||||
@@ -241,12 +242,14 @@ export function createNewGridItem(
|
||||
grid: GridItem[],
|
||||
id: string,
|
||||
data: AppComponent,
|
||||
columns?: Record<number, any>
|
||||
columns?: Record<number, any>,
|
||||
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
||||
recOverride?: Record<number, Size>
|
||||
): GridItem {
|
||||
const newComponent = {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: initialPosition.x,
|
||||
y: initialPosition.y,
|
||||
fullHeight: false
|
||||
}
|
||||
|
||||
@@ -260,7 +263,7 @@ export function createNewGridItem(
|
||||
|
||||
gridColumns.forEach((column) => {
|
||||
if (!columns) {
|
||||
const rec = getRecommendedDimensionsByComponent(newData.type, column)
|
||||
const rec = recOverride?.[column] ?? getRecommendedDimensionsByComponent(newData.type, column)
|
||||
|
||||
newItem[column] = {
|
||||
...newComponent,
|
||||
@@ -271,6 +274,7 @@ export function createNewGridItem(
|
||||
newItem[column] = columns[column]
|
||||
}
|
||||
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
|
||||
|
||||
newItem[column] = { ...newItem[column], ...position }
|
||||
})
|
||||
|
||||
@@ -325,7 +329,13 @@ export function cleanseOneOfConfiguration(
|
||||
export function appComponentFromType<T extends keyof typeof components>(
|
||||
type: T,
|
||||
overrideConfiguration?: Partial<InitialAppComponent['configuration']>,
|
||||
extra?: any
|
||||
extra?: any,
|
||||
override?: {
|
||||
customCss?: ComponentCustomCSS<T>
|
||||
verticalAlignment?: 'top' | 'center' | 'bottom'
|
||||
horizontalAlignment?: 'left' | 'center' | 'right'
|
||||
componnetInput?: Partial<InitialAppComponent['componentInput']>
|
||||
}
|
||||
): (id: string) => BaseAppComponent & BaseComponent<T> {
|
||||
return (id: string) => {
|
||||
const init = JSON.parse(JSON.stringify(ccomponents[type].initialData)) as InitialAppComponent
|
||||
@@ -351,19 +361,19 @@ export function appComponentFromType<T extends keyof typeof components>(
|
||||
type,
|
||||
//TODO remove tooltip from there
|
||||
configuration: deepMergeWithPriority(configuration, overrideConfiguration ?? {}),
|
||||
componentInput: init.componentInput,
|
||||
componentInput: override?.componnetInput ?? init.componentInput,
|
||||
panes: init.panes,
|
||||
tabs: init.tabs,
|
||||
conditions: init.conditions,
|
||||
nodes: init.nodes,
|
||||
customCss: ccomponents[type].customCss as any,
|
||||
customCss: deepMergeWithPriority(ccomponents[type].customCss as any, override?.customCss),
|
||||
recomputeIds: init.recomputeIds ? [] : undefined,
|
||||
actionButtons: init.actionButtons ? [] : undefined,
|
||||
actions: [],
|
||||
menuItems: init.menuItems ? [] : undefined,
|
||||
numberOfSubgrids: init.numberOfSubgrids,
|
||||
horizontalAlignment: init.horizontalAlignment,
|
||||
verticalAlignment: init.verticalAlignment,
|
||||
horizontalAlignment: override?.horizontalAlignment ?? init.horizontalAlignment,
|
||||
verticalAlignment: override?.verticalAlignment ?? init.verticalAlignment,
|
||||
id,
|
||||
...(extra ?? {})
|
||||
}
|
||||
@@ -374,7 +384,9 @@ export function insertNewGridItem(
|
||||
builddata: (id: string) => AppComponent,
|
||||
focusedGrid: FocusedGrid | undefined,
|
||||
columns?: Record<string, any>,
|
||||
keepId?: string
|
||||
keepId?: string,
|
||||
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
||||
recOverride?: Record<number, Size>
|
||||
): string {
|
||||
const id = keepId ?? getNextGridItemId(app)
|
||||
|
||||
@@ -421,7 +433,7 @@ export function insertNewGridItem(
|
||||
|
||||
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
||||
|
||||
const newItem = createNewGridItem(grid, id, data, columns)
|
||||
const newItem = createNewGridItem(grid, id, data, columns, initialPosition, recOverride)
|
||||
grid.push(newItem)
|
||||
return id
|
||||
}
|
||||
@@ -1026,3 +1038,94 @@ export function isTableAction(id: string, app: App): boolean {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function setUpTopBarComponentContent(id: string, app: App) {
|
||||
insertNewGridItem(
|
||||
app,
|
||||
appComponentFromType(
|
||||
'textcomponent',
|
||||
|
||||
{
|
||||
disableNoText: {
|
||||
value: true,
|
||||
type: 'static',
|
||||
fieldType: 'boolean'
|
||||
},
|
||||
tooltip: {
|
||||
type: 'evalv2',
|
||||
|
||||
fieldType: 'text',
|
||||
expr: '`Author: ${ctx.author}`',
|
||||
connections: [
|
||||
{
|
||||
componentId: 'ctx',
|
||||
id: 'author'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
customCss: {
|
||||
text: {
|
||||
class: 'text-xl font-semibold whitespace-nowrap truncate' as any,
|
||||
style: ''
|
||||
}
|
||||
},
|
||||
verticalAlignment: 'center',
|
||||
componnetInput: {
|
||||
type: 'templatev2',
|
||||
fieldType: 'template',
|
||||
eval: '${ctx.summary}',
|
||||
connections: [
|
||||
{
|
||||
id: 'summary',
|
||||
componentId: 'ctx'
|
||||
}
|
||||
] as InputConnectionEval[]
|
||||
}
|
||||
}
|
||||
) as (id: string) => AppComponent,
|
||||
{
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
3: {
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
12: {
|
||||
w: 6,
|
||||
h: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
insertNewGridItem(
|
||||
app,
|
||||
appComponentFromType('recomputeallcomponent', undefined, undefined, {
|
||||
horizontalAlignment: 'right'
|
||||
}) as (id: string) => AppComponent,
|
||||
{
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
3: {
|
||||
w: 3,
|
||||
h: 1
|
||||
},
|
||||
12: {
|
||||
w: 6,
|
||||
h: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
|
||||
import AppNavbar from '../../components/display/AppNavbar.svelte'
|
||||
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
|
||||
import AppDisplayComponentByJobId from '../../components/display/AppDisplayComponentByJobId.svelte'
|
||||
import AppDisplayComponentByJobId from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -856,6 +857,15 @@
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'recomputeallcomponent'}
|
||||
<AppRecomputeAll
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,9 @@ import {
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
CalendarClock,
|
||||
AppWindow
|
||||
AppWindow,
|
||||
PanelTop,
|
||||
RefreshCw
|
||||
} from 'lucide-svelte'
|
||||
import type {
|
||||
Aligned,
|
||||
@@ -279,6 +281,8 @@ export type NavBarComponent = BaseComponent<'navbarcomponent'> & {
|
||||
|
||||
export type DateSelectComponent = BaseComponent<'dateselectcomponent'>
|
||||
|
||||
export type RecomputeAllComponent = BaseComponent<'recomputeallcomponent'>
|
||||
|
||||
export type TypedComponent =
|
||||
| DBExplorerComponent
|
||||
| DisplayComponent
|
||||
@@ -356,6 +360,7 @@ export type TypedComponent =
|
||||
| NavBarComponent
|
||||
| DateSelectComponent
|
||||
| JobIdDisplayComponent
|
||||
| RecomputeAllComponent
|
||||
|
||||
export type AppComponent = BaseAppComponent & TypedComponent
|
||||
|
||||
@@ -368,7 +373,18 @@ export function getRecommendedDimensionsByComponent(
|
||||
componentType: AppComponent['type'],
|
||||
column: number
|
||||
): Size {
|
||||
const size = components[componentType].dims.split('-')[column === 3 ? 0 : 1].split(':')
|
||||
return processDimension(components[componentType].dims, column)
|
||||
}
|
||||
|
||||
export function processDimension(
|
||||
dimension: AppComponentDimensions | undefined,
|
||||
column: number
|
||||
): Size {
|
||||
if (!dimension) {
|
||||
return { w: 1, h: 1 }
|
||||
}
|
||||
|
||||
const size = dimension.split('-')[column === 3 ? 0 : 1].split(':')
|
||||
return { w: +size[0], h: +size[1] }
|
||||
}
|
||||
|
||||
@@ -398,6 +414,7 @@ export type PresetComponentConfig = {
|
||||
targetComponent: keyof typeof components
|
||||
configuration: object
|
||||
type: string
|
||||
dims?: AppComponentDimensions
|
||||
}
|
||||
|
||||
export interface InitialAppComponent extends Partial<Aligned> {
|
||||
@@ -1134,6 +1151,12 @@ export const components = {
|
||||
fieldType: 'text',
|
||||
|
||||
tooltip: 'Tooltip text if not empty'
|
||||
},
|
||||
disableNoText: {
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean',
|
||||
tooltip: 'Remove the "No text" placeholder'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4029,6 +4052,21 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
recomputeallcomponent: {
|
||||
name: 'Recompute all',
|
||||
icon: RefreshCw,
|
||||
documentationLink: `${documentationBaseUrl}/top_bar`,
|
||||
dims: '4:1-6:1' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { style: '', class: '' }
|
||||
},
|
||||
initialData: {
|
||||
...defaultAlignement,
|
||||
componentInput: undefined,
|
||||
configuration: {},
|
||||
menuItems: true
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -4054,6 +4092,14 @@ export const presetComponents = {
|
||||
}
|
||||
},
|
||||
type: 'invisibletabscomponent'
|
||||
},
|
||||
topbarcomponent: {
|
||||
name: 'Top Bar',
|
||||
icon: PanelTop,
|
||||
targetComponent: 'containercomponent' as const,
|
||||
configuration: {},
|
||||
type: 'topbarcomponent',
|
||||
dims: '6:2-12:2' as AppComponentDimensions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ const layout: ComponentSet = {
|
||||
'steppercomponent',
|
||||
'carousellistcomponent',
|
||||
'decisiontreecomponent',
|
||||
'navbarcomponent'
|
||||
]
|
||||
'navbarcomponent',
|
||||
'recomputeallcomponent'
|
||||
],
|
||||
presets: ['topbarcomponent']
|
||||
} as const
|
||||
|
||||
const buttons: ComponentSet = {
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
COMPONENT_SETS,
|
||||
type AppComponent,
|
||||
type TypedComponent,
|
||||
DEPRECATED_COMPONENTS
|
||||
DEPRECATED_COMPONENTS,
|
||||
processDimension
|
||||
} from '../component'
|
||||
import ListItem from './ListItem.svelte'
|
||||
import { appComponentFromType, copyComponent, insertNewGridItem } from '../appUtils'
|
||||
import {
|
||||
appComponentFromType,
|
||||
copyComponent,
|
||||
insertNewGridItem,
|
||||
setUpTopBarComponentContent
|
||||
} from '../appUtils'
|
||||
import { push } from '$lib/history'
|
||||
import { ClearableInput, Drawer, DrawerContent } from '../../../common'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
@@ -136,13 +142,30 @@
|
||||
|
||||
const id = insertNewGridItem(
|
||||
$app,
|
||||
appComponentFromType(preset.targetComponent, preset.configuration) as (
|
||||
id: string
|
||||
) => AppComponent,
|
||||
$focusedGrid
|
||||
appComponentFromType(preset.targetComponent, preset.configuration, undefined, {
|
||||
customCss: {
|
||||
container: {
|
||||
class: '!p-0' as any,
|
||||
style: ''
|
||||
}
|
||||
}
|
||||
}) as (id: string) => AppComponent,
|
||||
$focusedGrid,
|
||||
undefined,
|
||||
undefined,
|
||||
{ x: 0, y: 0 },
|
||||
{
|
||||
3: processDimension(preset.dims, 3),
|
||||
12: processDimension(preset.dims, 12)
|
||||
}
|
||||
)
|
||||
|
||||
$selectedComponent = [id]
|
||||
|
||||
if (appComponentType === 'topbarcomponent') {
|
||||
setUpTopBarComponentContent(id, $app)
|
||||
}
|
||||
|
||||
$app = $app
|
||||
}
|
||||
|
||||
@@ -150,7 +173,7 @@
|
||||
|
||||
$: componentsFiltered = COMPONENT_SETS.map((set) => ({
|
||||
...set,
|
||||
components: set.components.filter((component) => {
|
||||
components: set.components?.filter((component) => {
|
||||
const name = componentsRecord[component].name.toLowerCase()
|
||||
return name.includes(search.toLowerCase().trim())
|
||||
}),
|
||||
|
||||
@@ -796,5 +796,8 @@ export const quickStyleProperties: Record<
|
||||
jobiddisplaycomponent: {
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
recomputeallcomponent: {
|
||||
container: containerDefaultProps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { Preview } from '$lib/gen'
|
||||
import type { Policy, Preview } from '$lib/gen'
|
||||
import type { History } from '$lib/history'
|
||||
|
||||
import type { Writable } from 'svelte/store'
|
||||
@@ -154,11 +154,13 @@ export type App = {
|
||||
name: string
|
||||
inlineScript: InlineScript
|
||||
}>
|
||||
|
||||
//TODO: should be called hidden runnables but migration tbd
|
||||
hiddenInlineScripts: Array<HiddenRunnable>
|
||||
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
|
||||
subgrids?: Record<string, GridItem[]>
|
||||
theme: AppTheme | undefined
|
||||
hideLegacyTopBar?: boolean | undefined
|
||||
}
|
||||
|
||||
export type ConnectingInput = {
|
||||
@@ -271,6 +273,7 @@ export type AppViewerContext = {
|
||||
debuggingComponents: Writable<Record<string, number>>
|
||||
replaceStateFn?: ((url: string) => void) | undefined
|
||||
gotoFn?: ((url: string, opt?: Record<string, any> | undefined) => void) | undefined
|
||||
policy: Policy
|
||||
}
|
||||
|
||||
export type AppEditorContext = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppComponent } from './editor/component'
|
||||
import { type AppComponent } from './editor/component'
|
||||
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
|
||||
import type { Output } from './rx'
|
||||
import type {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
export let hasPadding: boolean = true
|
||||
export let target: string | undefined = 'body'
|
||||
export let disabled: boolean = false
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({ placement: 'auto' })
|
||||
|
||||
const popperOptions: PopperOptions<{}> = {
|
||||
@@ -28,7 +30,7 @@
|
||||
<span use:popperRef>
|
||||
<MenuButton
|
||||
class={twMerge('h-full w-full flex flex-row gap-2 items-center', hasPadding ? 'px-2' : '')}
|
||||
|
||||
{disabled}
|
||||
>
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
|
||||
@@ -556,7 +556,6 @@ export function deepMergeWithPriority<T>(target: T, source: T): T {
|
||||
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key) && merged?.hasOwnProperty(key)) {
|
||||
console.log(target)
|
||||
if (target?.hasOwnProperty(key)) {
|
||||
merged[key] = deepMergeWithPriority(target[key], source[key])
|
||||
} else {
|
||||
|
||||
@@ -8,9 +8,19 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import type { App } from '$lib/components/apps/types'
|
||||
import { afterNavigate, replaceState } from '$app/navigation'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { DEFAULT_THEME } from '$lib/components/apps/editor/componentsPanel/themeUtils'
|
||||
import {
|
||||
presets,
|
||||
processDimension,
|
||||
type AppComponent
|
||||
} from '$lib/components/apps/editor/component'
|
||||
import {
|
||||
appComponentFromType,
|
||||
insertNewGridItem,
|
||||
setUpTopBarComponentContent
|
||||
} from '$lib/components/apps/editor/appUtils'
|
||||
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
const hubId = $page.url.searchParams.get('hub')
|
||||
@@ -105,6 +115,34 @@
|
||||
}
|
||||
])
|
||||
value = decodeState(state)
|
||||
} else {
|
||||
const preset = presets['topbarcomponent']
|
||||
|
||||
const id = insertNewGridItem(
|
||||
value,
|
||||
appComponentFromType(preset.targetComponent, preset.configuration, undefined, {
|
||||
customCss: {
|
||||
container: {
|
||||
class: '!p-0' as any,
|
||||
style: ''
|
||||
}
|
||||
}
|
||||
}) as (id: string) => AppComponent,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ x: 0, y: 0 },
|
||||
{
|
||||
3: processDimension(preset.dims, 3),
|
||||
12: processDimension(preset.dims, 12)
|
||||
}
|
||||
)
|
||||
|
||||
setUpTopBarComponentContent(id, value)
|
||||
|
||||
value.hideLegacyTopBar = true
|
||||
|
||||
value = value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user