fix(frontend): fix home (#981)
This commit is contained in:
430
frontend/src/lib/components/home/ItemsList.svelte
Normal file
430
frontend/src/lib/components/home/ItemsList.svelte
Normal file
@@ -0,0 +1,430 @@
|
||||
<script lang="ts">
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
Job,
|
||||
JobService,
|
||||
ListableApp,
|
||||
Script,
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import VirtualList from '@sveltejs/svelte-virtual-list'
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Skeleton,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup
|
||||
} from '$lib/components/common'
|
||||
import { canWrite, classNames, getScriptByPath, pluralize } from '$lib/utils'
|
||||
import type { HubItem } from '$lib/components/flows/pickers/model'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { faCodeFork, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import { Code2, LayoutDashboard, Wind } from 'svelte-lucide'
|
||||
|
||||
import ScriptRow from '$lib/components/common/table/ScriptRow.svelte'
|
||||
import FlowRow from '$lib/components/common/table/FlowRow.svelte'
|
||||
import AppRow from '$lib/components/common/table/AppRow.svelte'
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
let jobs: Job[] = []
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app'> = T & {
|
||||
canWrite: boolean
|
||||
marked?: string
|
||||
type?: U
|
||||
time?: number
|
||||
starred?: boolean
|
||||
}
|
||||
|
||||
type TableScript = TableItem<Script, 'script'>
|
||||
type TableFlow = TableItem<Flow, 'flow'>
|
||||
type TableApp = TableItem<ListableApp, 'app'>
|
||||
|
||||
let scripts: TableScript[] | undefined
|
||||
let flows: TableFlow[] | undefined
|
||||
let apps: TableApp[] | undefined
|
||||
|
||||
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
|
||||
|
||||
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
|
||||
|
||||
let filter: string = ''
|
||||
let shareModalScripts: ShareModal
|
||||
let shareModalFlows: ShareModal
|
||||
|
||||
let loading = true
|
||||
let flowViewer: Drawer
|
||||
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
|
||||
|
||||
let codeViewer: Drawer
|
||||
let codeViewerContent: string = ''
|
||||
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
|
||||
let codeViewerObj: HubItem | undefined = undefined
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
perPage: 300
|
||||
})
|
||||
|
||||
scripts = loadedScripts.map((script: Script) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(script.path, script.extra_perms, $userStore) &&
|
||||
script.workspace_id == $workspaceStore,
|
||||
...script
|
||||
}
|
||||
})
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function loadFlows(): Promise<void> {
|
||||
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
|
||||
return {
|
||||
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
...x
|
||||
}
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function loadApps(): Promise<void> {
|
||||
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
|
||||
...app
|
||||
}
|
||||
})
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: owners = Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
|
||||
|
||||
$: combinedItems =
|
||||
flows == undefined || scripts == undefined || apps == undefined
|
||||
? undefined
|
||||
: [
|
||||
...flows.map((x) => ({
|
||||
...x,
|
||||
type: 'flow' as 'flow',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
})),
|
||||
...scripts.map((x) => ({
|
||||
...x,
|
||||
type: 'script' as 'script',
|
||||
time: new Date(x.created_at).getTime()
|
||||
})),
|
||||
...apps.map((x) => ({
|
||||
...x,
|
||||
type: 'app' as 'app',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
}))
|
||||
].sort((a, b) =>
|
||||
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
|
||||
)
|
||||
|
||||
$: preFilteredItems =
|
||||
ownerFilter != undefined
|
||||
? combinedItems?.filter(
|
||||
(x) => x.path.startsWith(ownerFilter ?? '') && (x.type == itemKind || itemKind == 'all')
|
||||
)
|
||||
: combinedItems?.filter((x) => x.type == itemKind || itemKind == 'all')
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
async function loadJobs() {
|
||||
jobs = await JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
success: true,
|
||||
createdBy: $userStore?.username,
|
||||
jobKinds: 'flow,script'
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: {
|
||||
if (($userStore || $superadmin) && $workspaceStore) {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadJobs()
|
||||
}
|
||||
}
|
||||
const cmp = new Intl.Collator('en').compare
|
||||
|
||||
const opts: uFuzzy.Options = {
|
||||
sort: (info, haystack, needle) => {
|
||||
let {
|
||||
idx,
|
||||
chars,
|
||||
terms,
|
||||
interLft2,
|
||||
interLft1,
|
||||
// interRgt2,
|
||||
// interRgt1,
|
||||
start,
|
||||
intraIns,
|
||||
interIns
|
||||
} = info
|
||||
|
||||
const sortResult = idx
|
||||
.map((v, i) => i)
|
||||
.sort(
|
||||
(ia, ib) =>
|
||||
// most contig chars matched
|
||||
chars[ib] - chars[ia] ||
|
||||
// least char intra-fuzz (most contiguous)
|
||||
intraIns[ia] - intraIns[ib] ||
|
||||
// most prefix bounds, boosted by full term matches
|
||||
terms[ib] +
|
||||
interLft2[ib] +
|
||||
0.5 * interLft1[ib] -
|
||||
(terms[ia] + interLft2[ia] + 0.5 * interLft1[ia]) ||
|
||||
// highest density of match (least span)
|
||||
// span[ia] - span[ib] ||
|
||||
// highest density of match (least term inter-fuzz)
|
||||
interIns[ia] - interIns[ib] ||
|
||||
// earliest start of match
|
||||
start[ia] - start[ib] ||
|
||||
// alphabetic
|
||||
cmp(haystack[idx[ia]], haystack[idx[ib]]) +
|
||||
(preFilteredItems?.[idx[ib]]?.starred ? 100 : 0) -
|
||||
(preFilteredItems?.[idx[ia]]?.starred ? 100 : 0)
|
||||
)
|
||||
return sortResult
|
||||
}
|
||||
}
|
||||
|
||||
$: items = filter !== '' ? filteredItems : preFilteredItems
|
||||
let start: number
|
||||
let end: number
|
||||
|
||||
export let clientHeight: number
|
||||
|
||||
$: height = (100 * clientHeight) / window.innerHeight
|
||||
|
||||
function resetScroll() {
|
||||
const element = document.getElementsByTagName('svelte-virtual-list-viewport')
|
||||
const firstElement = element.item(0)
|
||||
if (firstElement) {
|
||||
firstElement.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
$: items && resetScroll()
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={preFilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (x.summary ? x.summary + ' (' + x.path + ')' : x.path)}
|
||||
{opts}
|
||||
/>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModalScripts}
|
||||
kind="script"
|
||||
on:change={() => {
|
||||
loadScripts()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModalFlows}
|
||||
kind="flow"
|
||||
on:change={() => {
|
||||
loadFlows()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer bind:this={codeViewer} size="900px">
|
||||
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
|
||||
<div slot="submission" class="flex flex-row gap-2 pr-2">
|
||||
<Button
|
||||
href="https://hub.windmill.dev/scripts/{codeViewerObj?.app ?? ''}/{codeViewerObj?.ask_id ??
|
||||
0}"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
variant="border"
|
||||
>
|
||||
View on the Hub
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/add?hub={encodeURIComponent(codeViewerObj?.path ?? '')}"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<HighlightCode language={codeViewerLanguage} code={codeViewerContent} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={flowViewer} size="900px">
|
||||
<DrawerContent title="Hub flow" on:close={flowViewer.closeDrawer}>
|
||||
<div slot="submission" class="flex flex-row gap-2 pr-2">
|
||||
<Button
|
||||
href="https://hub.windmill.dev/flows/{flowViewerFlow?.flow?.id}"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
variant="border"
|
||||
>
|
||||
View on the Hub
|
||||
</Button>
|
||||
<Button href="/flows/add?hub={flowViewerFlow?.flow?.id}" startIcon={{ icon: faCodeFork }}>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if flowViewerFlow?.flow}
|
||||
<FlowViewer flow={flowViewerFlow.flow} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<CenteredPage>
|
||||
<div class="flex flex-col md:flex-row gap-2 items-center sm:justify-between w-full">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={itemKind}>
|
||||
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
|
||||
<ToggleButton light position="center" value="script" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size="16px" />
|
||||
Scripts
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="center" value="flow" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Wind size="16px" />
|
||||
Flows
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="right" value="app" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<LayoutDashboard size="16px" />
|
||||
Apps
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="relative text-gray-600 w-full">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
placeholder="Search Scripts, Flows & Apps"
|
||||
bind:value={filter}
|
||||
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
|
||||
/>
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if owners.length > 0}
|
||||
<div class="gap-2 w-full flex flex-wrap my-4">
|
||||
{#each owners as owner (owner)}
|
||||
<div in:fade animate:flip={{ duration: 200 }}>
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
ownerFilter = ownerFilter == owner ? undefined : owner
|
||||
}}
|
||||
color={owner === ownerFilter ? 'blue' : 'gray'}
|
||||
baseClass={owner === ownerFilter ? 'border border-blue-500' : 'border'}
|
||||
>
|
||||
{owner}
|
||||
{#if owner === ownerFilter}✗{/if}
|
||||
</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if filteredItems == undefined}
|
||||
<div class="mt-4" />
|
||||
<Skeleton layout={[[2], 1]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{:else if filteredItems.length === 0}
|
||||
<div class="flex justify-center items-center h-48">
|
||||
<div class="text-gray-500 text-center">
|
||||
<div class="text-2xl font-bold">No items found</div>
|
||||
<div class="text-sm">Try changing your search or filters</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class={classNames('border rounded-md')}
|
||||
style={`height: calc(100vh - 10em - ${height}vh);`}
|
||||
>
|
||||
<VirtualList {items} let:item bind:start bind:end>
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadScripts}
|
||||
script={item}
|
||||
shareModal={shareModalScripts}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadFlows}
|
||||
flow={item}
|
||||
shareModal={shareModalFlows}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadApps}
|
||||
app={item}
|
||||
/>
|
||||
{/if}
|
||||
</VirtualList>
|
||||
</div>
|
||||
<span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
|
||||
<span class="text-xs">{`(${start} - ${end})`}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
@@ -1,83 +1,28 @@
|
||||
<script lang="ts">
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
Job,
|
||||
JobService,
|
||||
ListableApp,
|
||||
Script,
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import VirtualList from '@sveltejs/svelte-virtual-list'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Skeleton,
|
||||
Tab,
|
||||
Tabs,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup
|
||||
} from '$lib/components/common'
|
||||
import { FlowService, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import CreateActionsFlow from '$lib/components/flows/CreateActionsFlow.svelte'
|
||||
import CreateActionsScript from '$lib/components/scripts/CreateActionsScript.svelte'
|
||||
import { canWrite, classNames, getScriptByPath, pluralize } from '$lib/utils'
|
||||
import { getScriptByPath } from '$lib/utils'
|
||||
import type { HubItem } from '$lib/components/flows/pickers/model'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { faCodeFork, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import PickHubFlow from './PickHubFlow.svelte'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import { Building, Code2, Globe2, LayoutDashboard, Wind } from 'svelte-lucide'
|
||||
import { Building, Globe2 } from 'svelte-lucide'
|
||||
|
||||
import ScriptRow from '$lib/components/common/table/ScriptRow.svelte'
|
||||
import FlowRow from '$lib/components/common/table/FlowRow.svelte'
|
||||
import AppRow from '$lib/components/common/table/AppRow.svelte'
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
import { flip } from 'svelte/animate'
|
||||
import CreateApp from '$lib/components/apps/CreateApp.svelte'
|
||||
|
||||
let jobs: Job[] = []
|
||||
import ItemsList from '$lib/components/home/ItemsList.svelte'
|
||||
|
||||
type Tab = 'hubscripts' | 'hubflows' | 'workspace'
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app'> = T & {
|
||||
canWrite: boolean
|
||||
marked?: string
|
||||
type?: U
|
||||
time?: number
|
||||
starred?: boolean
|
||||
}
|
||||
|
||||
type TableScript = TableItem<Script, 'script'>
|
||||
type TableFlow = TableItem<Flow, 'flow'>
|
||||
type TableApp = TableItem<ListableApp, 'app'>
|
||||
|
||||
let scripts: TableScript[] | undefined
|
||||
let flows: TableFlow[] | undefined
|
||||
let apps: TableApp[] | undefined
|
||||
|
||||
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
|
||||
|
||||
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
|
||||
|
||||
let tab: Tab = 'workspace'
|
||||
let filter: string = ''
|
||||
let shareModalScripts: ShareModal
|
||||
let shareModalFlows: ShareModal
|
||||
|
||||
let loading = true
|
||||
let flowViewer: Drawer
|
||||
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
|
||||
|
||||
@@ -86,24 +31,6 @@
|
||||
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
|
||||
let codeViewerObj: HubItem | undefined = undefined
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
perPage: 300
|
||||
})
|
||||
|
||||
scripts = loadedScripts.map((script: Script) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(script.path, script.extra_perms, $userStore) &&
|
||||
script.workspace_id == $workspaceStore,
|
||||
...script
|
||||
}
|
||||
})
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function viewCode(obj: HubItem) {
|
||||
const { content, language } = await getScriptByPath(obj.path)
|
||||
codeViewerContent = content
|
||||
@@ -112,178 +39,15 @@
|
||||
codeViewer.openDrawer()
|
||||
}
|
||||
|
||||
async function loadFlows(): Promise<void> {
|
||||
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
|
||||
return {
|
||||
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
...x
|
||||
}
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function loadApps(): Promise<void> {
|
||||
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
|
||||
...app
|
||||
}
|
||||
})
|
||||
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function viewFlow(obj: { flow_id: number }): Promise<void> {
|
||||
const hub = await FlowService.getHubFlowById({ id: obj.flow_id })
|
||||
flowViewerFlow = hub
|
||||
flowViewer.openDrawer()
|
||||
}
|
||||
|
||||
$: owners = Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
|
||||
|
||||
$: combinedItems =
|
||||
flows == undefined || scripts == undefined || apps == undefined
|
||||
? undefined
|
||||
: [
|
||||
...flows.map((x) => ({
|
||||
...x,
|
||||
type: 'flow' as 'flow',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
})),
|
||||
...scripts.map((x) => ({
|
||||
...x,
|
||||
type: 'script' as 'script',
|
||||
time: new Date(x.created_at).getTime()
|
||||
})),
|
||||
...apps.map((x) => ({
|
||||
...x,
|
||||
type: 'app' as 'app',
|
||||
time: new Date(x.edited_at).getTime()
|
||||
}))
|
||||
].sort((a, b) =>
|
||||
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
|
||||
)
|
||||
|
||||
$: preFilteredItems =
|
||||
ownerFilter != undefined
|
||||
? combinedItems?.filter(
|
||||
(x) => x.path.startsWith(ownerFilter ?? '') && (x.type == itemKind || itemKind == 'all')
|
||||
)
|
||||
: combinedItems?.filter((x) => x.type == itemKind || itemKind == 'all')
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
async function loadJobs() {
|
||||
jobs = await JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
success: true,
|
||||
createdBy: $userStore?.username,
|
||||
jobKinds: 'flow,script'
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: {
|
||||
if (($userStore || $superadmin) && $workspaceStore) {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadJobs()
|
||||
}
|
||||
}
|
||||
const cmp = new Intl.Collator('en').compare
|
||||
|
||||
const opts: uFuzzy.Options = {
|
||||
sort: (info, haystack, needle) => {
|
||||
let {
|
||||
idx,
|
||||
chars,
|
||||
terms,
|
||||
interLft2,
|
||||
interLft1,
|
||||
// interRgt2,
|
||||
// interRgt1,
|
||||
start,
|
||||
intraIns,
|
||||
interIns
|
||||
} = info
|
||||
|
||||
const sortResult = idx
|
||||
.map((v, i) => i)
|
||||
.sort(
|
||||
(ia, ib) =>
|
||||
// most contig chars matched
|
||||
chars[ib] - chars[ia] ||
|
||||
// least char intra-fuzz (most contiguous)
|
||||
intraIns[ia] - intraIns[ib] ||
|
||||
// most prefix bounds, boosted by full term matches
|
||||
terms[ib] +
|
||||
interLft2[ib] +
|
||||
0.5 * interLft1[ib] -
|
||||
(terms[ia] + interLft2[ia] + 0.5 * interLft1[ia]) ||
|
||||
// highest density of match (least span)
|
||||
// span[ia] - span[ib] ||
|
||||
// highest density of match (least term inter-fuzz)
|
||||
interIns[ia] - interIns[ib] ||
|
||||
// earliest start of match
|
||||
start[ia] - start[ib] ||
|
||||
// alphabetic
|
||||
cmp(haystack[idx[ia]], haystack[idx[ib]]) +
|
||||
(preFilteredItems?.[idx[ib]]?.starred ? 100 : 0) -
|
||||
(preFilteredItems?.[idx[ia]]?.starred ? 100 : 0)
|
||||
)
|
||||
return sortResult
|
||||
}
|
||||
}
|
||||
|
||||
$: items = filter !== '' ? filteredItems : preFilteredItems
|
||||
let start: number
|
||||
let end: number
|
||||
|
||||
$: clientHeight = 0
|
||||
|
||||
$: height = (100 * clientHeight) / window.innerHeight
|
||||
|
||||
function resetScroll() {
|
||||
const element = document.getElementsByTagName('svelte-virtual-list-viewport')
|
||||
const firstElement = element.item(0)
|
||||
if (firstElement) {
|
||||
firstElement.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
$: items && resetScroll()
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={preFilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (x.summary ? x.summary + ' (' + x.path + ')' : x.path)}
|
||||
{opts}
|
||||
/>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModalScripts}
|
||||
kind="script"
|
||||
on:change={() => {
|
||||
loadScripts()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModalFlows}
|
||||
kind="flow"
|
||||
on:change={() => {
|
||||
loadFlows()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer bind:this={codeViewer} size="900px">
|
||||
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
|
||||
<div slot="submission" class="flex flex-row gap-2 pr-2">
|
||||
@@ -386,130 +150,5 @@
|
||||
</div>
|
||||
|
||||
{#if tab == 'workspace'}
|
||||
<CenteredPage>
|
||||
<div class="flex flex-col md:flex-row gap-2 items-center sm:justify-between w-full">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={itemKind}>
|
||||
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
|
||||
<ToggleButton light position="center" value="script" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size="16px" />
|
||||
Scripts
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="center" value="flow" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Wind size="16px" />
|
||||
Flows
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="right" value="app" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<LayoutDashboard size="16px" />
|
||||
Apps
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="relative text-gray-600 w-full">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
placeholder="Search Scripts, Flows & Apps"
|
||||
bind:value={filter}
|
||||
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
|
||||
/>
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if owners.length > 0}
|
||||
<div class="gap-2 w-full flex flex-wrap my-4">
|
||||
{#each owners as owner (owner)}
|
||||
<div in:fade animate:flip={{ duration: 200 }}>
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
ownerFilter = ownerFilter == owner ? undefined : owner
|
||||
}}
|
||||
color={owner === ownerFilter ? 'blue' : 'gray'}
|
||||
baseClass={owner === ownerFilter ? 'border border-blue-500' : 'border'}
|
||||
>
|
||||
{owner}
|
||||
{#if owner === ownerFilter}✗{/if}
|
||||
</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if filteredItems == undefined}
|
||||
<div class="mt-4" />
|
||||
<Skeleton layout={[[2], 1]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{:else if filteredItems.length === 0}
|
||||
<div class="flex justify-center items-center h-48">
|
||||
<div class="text-gray-500 text-center">
|
||||
<div class="text-2xl font-bold">No items found</div>
|
||||
<div class="text-sm">Try changing your search or filters</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class={classNames('border rounded-md')}
|
||||
style={`height: calc(100vh - 10em - ${height}vh);`}
|
||||
>
|
||||
<VirtualList {items} let:item bind:start bind:end>
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadScripts}
|
||||
script={item}
|
||||
shareModal={shareModalScripts}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadFlows}
|
||||
flow={item}
|
||||
shareModal={shareModalFlows}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadApps}
|
||||
app={item}
|
||||
/>
|
||||
{/if}
|
||||
</VirtualList>
|
||||
</div>
|
||||
<span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
|
||||
<span class="text-xs">{`(${start} - ${end})`}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
<ItemsList {clientHeight} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user