mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
skeletons for home search
This commit is contained in:
@@ -60,72 +60,70 @@
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[2], 0.7]} />
|
||||
{/each}
|
||||
{:else}
|
||||
{#if !items?.length}
|
||||
<div class="text-center text-sm text-gray-600 mt-2">
|
||||
{@html noItemMessage}
|
||||
</div>
|
||||
{:else if filteredItems?.length}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#each filteredItems as obj}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="py-4 px-1 gap-1 flex flex-row grow border border-gray-300 border-opacity-0
|
||||
{:else if !items?.length}
|
||||
<div class="text-center text-sm text-gray-600 mt-2">
|
||||
{@html noItemMessage}
|
||||
</div>
|
||||
{:else if filteredItems?.length}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#each filteredItems as obj}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="py-4 px-1 gap-1 flex flex-row grow border border-gray-300 border-opacity-0
|
||||
hover:bg-white hover:border-opacity-100 text-black"
|
||||
on:click={() => {
|
||||
if (closeOnClick) {
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
pickCallback(obj['path'], obj[extraField])
|
||||
}}
|
||||
>
|
||||
{#if `app` in obj}
|
||||
<div class="mr-2 text-sm text-left truncate w-24">
|
||||
<IconedResourceType after={true} silent={false} name={obj['app']} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if `resource_type` in obj}
|
||||
<div class="mr-2 truncate text-left w-24 text-sm">
|
||||
<IconedResourceType after={true} name={obj['resource_type']} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm font-semibold flex flex-col">
|
||||
<span class="mr-2 text-left">{obj[extraField] ?? ''}</span>
|
||||
{#if extraField != 'path'}
|
||||
<span class="font-normal text-xs text-left italic overflow-hidden"
|
||||
>{obj['path'] ?? ''}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if extraField != 'description'}
|
||||
<div class="text-xs font-light italic text-left">{obj['description'] ?? ''}</div
|
||||
on:click={() => {
|
||||
if (closeOnClick) {
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
pickCallback(obj['path'], obj[extraField])
|
||||
}}
|
||||
>
|
||||
{#if `app` in obj}
|
||||
<div class="mr-2 text-sm text-left truncate w-24">
|
||||
<IconedResourceType after={true} silent={false} name={obj['app']} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if `resource_type` in obj}
|
||||
<div class="mr-2 truncate text-left w-24 text-sm">
|
||||
<IconedResourceType after={true} name={obj['resource_type']} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm font-semibold flex flex-col">
|
||||
<span class="mr-2 text-left">{obj[extraField] ?? ''}</span>
|
||||
{#if extraField != 'path'}
|
||||
<span class="font-normal text-xs text-left italic overflow-hidden"
|
||||
>{obj['path'] ?? ''}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{#if buttons}
|
||||
<div class="flex flex-row">
|
||||
{#each Object.entries(buttons) as [name, button]}
|
||||
<button
|
||||
class="py-4 px-1 gap-1 flex flex-row grow text-black"
|
||||
on:click={() => {
|
||||
button(obj['path'] ?? '')
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="text-center text-sm text-gray-600 mt-2">
|
||||
{@html emptySearchMessage}
|
||||
</div>
|
||||
{/if}
|
||||
{#if extraField != 'description'}
|
||||
<div class="text-xs font-light italic text-left">{obj['description'] ?? ''}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{#if buttons}
|
||||
<div class="flex flex-row">
|
||||
{#each Object.entries(buttons) as [name, button]}
|
||||
<button
|
||||
class="py-4 px-1 gap-1 flex flex-row grow text-black"
|
||||
on:click={() => {
|
||||
button(obj['path'] ?? '')
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="text-center text-sm text-gray-600 mt-2">
|
||||
{@html emptySearchMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span slot="submission" class="mr-2">
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import uFuzzy from '@leeoniya/ufuzzy'
|
||||
|
||||
export let filter: string = ''
|
||||
export let items: any[]
|
||||
export let items: any[] | undefined
|
||||
export let f: (item: any) => string
|
||||
export let filteredItems: (any & { marked: string })[]
|
||||
export let filteredItems: (any & { marked: string })[] | undefined
|
||||
export let opts: uFuzzy.Options = {}
|
||||
|
||||
let uf = new uFuzzy(opts)
|
||||
$: plaintextItems = items.map((item) => f(item))
|
||||
$: plaintextItems = items?.map((item) => f(item)) ?? []
|
||||
|
||||
$: plaintextItems && filter != undefined && setTimeout(() => filterItems(), 0)
|
||||
|
||||
function filterItems() {
|
||||
if (filter.length == 0) {
|
||||
if (items == undefined || filter.length == 0) {
|
||||
filteredItems = items
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Skeleton,
|
||||
Tab,
|
||||
Tabs,
|
||||
ToggleButton,
|
||||
@@ -63,9 +64,9 @@
|
||||
type TableFlow = TableItem<Flow, 'flow'>
|
||||
type TableApp = TableItem<ListableApp, 'app'>
|
||||
|
||||
let scripts: TableScript[] = []
|
||||
let flows: TableFlow[] = []
|
||||
let apps: TableApp[] = []
|
||||
let scripts: TableScript[] | undefined
|
||||
let flows: TableFlow[] | undefined
|
||||
let apps: TableApp[] | undefined
|
||||
|
||||
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
|
||||
|
||||
@@ -143,24 +144,37 @@
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
let combinedItems: (TableScript | TableFlow | TableApp)[] = []
|
||||
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
|
||||
|
||||
$: combinedItems = [
|
||||
...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))
|
||||
$: 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(
|
||||
? combinedItems?.filter(
|
||||
(x) => x.path.startsWith(ownerFilter ?? '') && (x.type == itemKind || itemKind == 'all')
|
||||
)
|
||||
: combinedItems.filter((x) => x.type == itemKind || itemKind == 'all')
|
||||
: combinedItems?.filter((x) => x.type == itemKind || itemKind == 'all')
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
@@ -220,8 +234,8 @@
|
||||
start[ia] - start[ib] ||
|
||||
// alphabetic
|
||||
cmp(haystack[idx[ia]], haystack[idx[ib]]) +
|
||||
(preFilteredItems[idx[ib]].starred ? 100 : 0) -
|
||||
(preFilteredItems[idx[ia]].starred ? 100 : 0)
|
||||
(preFilteredItems?.[idx[ib]]?.starred ? 100 : 0) -
|
||||
(preFilteredItems?.[idx[ia]]?.starred ? 100 : 0)
|
||||
)
|
||||
return sortResult
|
||||
}
|
||||
@@ -448,7 +462,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if filteredItems.length === 0}
|
||||
{#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>
|
||||
@@ -487,7 +507,7 @@
|
||||
{/if}
|
||||
</VirtualList>
|
||||
</div>
|
||||
<span class="text-xs">{pluralize(items.length, 'item')}</span>
|
||||
<span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
|
||||
<span class="text-xs">{`(${start} - ${end})`}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import InviteUser from '$lib/components/InviteUser.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { faScroll, faWind } from '@fortawesome/free-solid-svg-icons'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
|
||||
let users: User[] = []
|
||||
let users: User[] | undefined = undefined
|
||||
let invites: WorkspaceInvite[] = []
|
||||
let filteredUsers: User[] = []
|
||||
let filteredUsers: User[] | undefined = undefined
|
||||
let userFilter = ''
|
||||
let scriptPath: string
|
||||
let initialPath: string
|
||||
@@ -120,44 +120,56 @@
|
||||
<th colspan="3">jobs & flows (<abbr title="past two weeks">2w</abbr>)</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each filteredUsers as { email, username, is_admin, usage } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{username}</td>
|
||||
<td>{is_admin ? 'admin' : 'user'}</td>
|
||||
<td>{usage?.jobs}</td>
|
||||
<td>{usage?.flows}</td>
|
||||
<td>{msToSec(usage?.duration_ms)}s</td>
|
||||
<td class="whitespace-nowrap"
|
||||
><button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}}>remove</button
|
||||
{#if filteredUsers}
|
||||
{#each filteredUsers as { email, username, is_admin, usage } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{username}</td>
|
||||
<td>{is_admin ? 'admin' : 'user'}</td>
|
||||
<td>{usage?.jobs}</td>
|
||||
<td>{usage?.flows}</td>
|
||||
<td>{msToSec(usage?.duration_ms)}s</td>
|
||||
<td class="whitespace-nowrap"
|
||||
><button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}}>remove</button
|
||||
>
|
||||
-
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: {
|
||||
is_admin: !is_admin
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
}}>{is_admin ? 'demote' : 'promote'}</button
|
||||
></td
|
||||
>
|
||||
-
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: {
|
||||
is_admin: !is_admin
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
}}>{is_admin ? 'demote' : 'promote'}</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each new Array(6) as _}
|
||||
<tr class="border">
|
||||
{#each new Array(4) as _}
|
||||
<td>
|
||||
<Skeleton layout={[[2]]} />
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user