mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
feat: overhaul scripts and flows page
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="ml-2 {relative ? 'relative' : ''} {$$props.class}"
|
||||
class="{relative ? 'relative' : ''} {$$props.class}"
|
||||
use:clickOutside
|
||||
on:click_outside={handleClickOutsideMenu}
|
||||
>
|
||||
|
||||
@@ -68,7 +68,9 @@
|
||||
const workspaceScripts: { path: string; summary?: string }[] = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore ?? 'NO_W'
|
||||
})
|
||||
await loadHubScripts()
|
||||
if (!$hubScripts) {
|
||||
await loadHubScripts()
|
||||
}
|
||||
const hubScripts_ = $hubScripts ?? []
|
||||
|
||||
return workspaceScripts.concat(hubScripts_)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import uFuzzy from '@leeoniya/ufuzzy'
|
||||
|
||||
export let filter: string = ''
|
||||
export let items: any[]
|
||||
export let f: (item: any) => string
|
||||
export let filteredItems: (any & { marked: string })[]
|
||||
|
||||
let opts = {}
|
||||
let uf = new uFuzzy(opts)
|
||||
$: plaintextItems = items.map((item) => f(item))
|
||||
|
||||
$: plaintextItems && filter != undefined && setTimeout(() => filterItems(), 100)
|
||||
|
||||
function filterItems() {
|
||||
if (filter.length == 0) {
|
||||
filteredItems = items
|
||||
return
|
||||
}
|
||||
// pre-filter
|
||||
let idxs = uf.filter(plaintextItems, filter)
|
||||
|
||||
let info = uf.info(idxs, plaintextItems, filter)
|
||||
let order = uf.sort(info, plaintextItems, filter)
|
||||
|
||||
let result: any[] = []
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
let infoIdx = order[i]
|
||||
result.push({
|
||||
...items[info.idx[infoIdx]],
|
||||
marked: uFuzzy.highlight(plaintextItems[info.idx[infoIdx]], info.ranges[infoIdx])
|
||||
})
|
||||
}
|
||||
filteredItems = result
|
||||
}
|
||||
</script>
|
||||
@@ -23,7 +23,7 @@ export namespace ButtonType {
|
||||
export const SpacingClasses: Record<ButtonType.Size, string> = {
|
||||
xs: 'px-3 py-1.5',
|
||||
sm: 'px-3 py-1.5',
|
||||
md: 'px-4 py-2',
|
||||
md: 'px-3 py-1.5',
|
||||
lg: 'px-4 py-2',
|
||||
xl: 'px-4 py-2'
|
||||
} as const
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { hubScripts } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import type { HubItem } from './model'
|
||||
import Fuse from 'fuse.js'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import { Badge } from '$lib/components/common'
|
||||
import { Badge, Skeleton } from '$lib/components/common'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { loadHubScripts } from '$lib/utils'
|
||||
|
||||
export let kind: 'script' | 'trigger' | 'approval' | 'failure' = 'script'
|
||||
|
||||
let items: HubItem[] = []
|
||||
$: items = ($hubScripts ?? []).filter((i) => i.kind === kind)
|
||||
|
||||
let filteredItems: HubItem[] | undefined = []
|
||||
let itemsFilter = ''
|
||||
let filteredItems: (HubItem & { marked?: string })[] = []
|
||||
let filter = ''
|
||||
let appFilter: string | undefined = undefined
|
||||
|
||||
const fuseOptions = {
|
||||
includeScore: false,
|
||||
keys: ['path', 'summay']
|
||||
}
|
||||
const fuse: Fuse<HubItem> = new Fuse(items, fuseOptions)
|
||||
|
||||
$: {
|
||||
items =
|
||||
$hubScripts?.filter(
|
||||
(x) => x.kind == kind && (appFilter == undefined || x.app == appFilter)
|
||||
) ?? []
|
||||
fuse.setCollection(items)
|
||||
}
|
||||
|
||||
$: filteredItems =
|
||||
itemsFilter.length > 0 && items ? fuse.search(itemsFilter).map((value) => value.item) : items
|
||||
|
||||
$: apps = Array.from(new Set(filteredItems?.map((x) => x.app) ?? []))
|
||||
$: apps = Array.from(new Set(filteredItems?.map((x) => x.app) ?? [])).sort()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
if (!$hubScripts) {
|
||||
loadHubScripts()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems {filter} {items} bind:filteredItems f={(x) => x.summary} />
|
||||
|
||||
<div class="flex flex-col min-h-0">
|
||||
<div class="w-12/12 pb-2">
|
||||
<input type="text" placeholder="Search script" bind:value={itemsFilter} class="search-item" />
|
||||
<div class="w-12/12 pb-2 flex flex-row mt-1 gap-1">
|
||||
<input type="text" placeholder="Search Scripts" bind:value={filter} class="text-2xl grow" />
|
||||
</div>
|
||||
|
||||
<div class="gap-2 w-full flex flex-wrap pb-2">
|
||||
@@ -56,9 +48,12 @@
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{#if filteredItems}
|
||||
<div class="overflow-auto">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<div class="overflow-auto">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#if $hubScripts}
|
||||
{#if filter.length > 0 && filteredItems.length == 0}
|
||||
<p>No items found</p>
|
||||
{/if}
|
||||
{#each filteredItems as obj}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
@@ -67,14 +62,24 @@
|
||||
dispatch('pick', obj)
|
||||
}}
|
||||
>
|
||||
<div class="mr-2 text-sm text-left truncate w-36 shrink-0">
|
||||
<div class="mr-2 text-sm text-left truncate w-32 shrink-0">
|
||||
<IconedResourceType after={true} silent={false} name={obj['app']} />
|
||||
</div>
|
||||
<div class="mr-2 text-left">{obj['summary'] ?? ''}</div>
|
||||
<div class="mr-2 text-left">
|
||||
{#if obj.marked}
|
||||
{@html obj.marked ?? ''}
|
||||
{:else}
|
||||
{obj.summary ?? ''}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as sk}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,18 +5,10 @@
|
||||
import HatIcon from '../icons/HatIcon.svelte'
|
||||
</script>
|
||||
|
||||
<div class="p-4 border border-gray-300 rounded-md bg-gray-50 overflow-auto">
|
||||
<div class="flex items-center">
|
||||
<h3 class="inline-flex items-center font-semibold">
|
||||
<HatIcon class="w-6 h-6 mr-2" />
|
||||
Getting started
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-2 mb-4 text-sm text-gray-700 ">
|
||||
<div class=" overflow-auto">
|
||||
<div class="mt-2 mb-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
Flows allow you to streamline complex processes and operations by chaining simple steps
|
||||
together. Each Flow is composed of one or more steps. Create a new flow or find inspiration on
|
||||
the Hub!
|
||||
</div>
|
||||
together. Each Flow is composed of one or more steps.
|
||||
<div class="flex flex-row flex-wrap gap-y-2">
|
||||
<Button href="/flows/add" color="dark" size="xs" btnClasses="mr-2" startIcon={{ icon: faPlus }}>
|
||||
Create flow
|
||||
|
||||
@@ -5,13 +5,7 @@
|
||||
import HatIcon from '../icons/HatIcon.svelte'
|
||||
</script>
|
||||
|
||||
<div class="p-4 border border-gray-300 rounded-md bg-gray-50 dark:border-gray-600 dark:bg-gray-700">
|
||||
<div class="flex items-center">
|
||||
<h3 class="inline-flex items-center font-semibold">
|
||||
<HatIcon class="w-6 h-6 mr-2" />
|
||||
Getting started
|
||||
</h3>
|
||||
</div>
|
||||
<div class="fle flex-row gap-2">
|
||||
<div class="mt-2 mb-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
Connect to apps like Slack, Google Drive or Airtable using OAuth.
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { truncateHash } from '$lib/utils'
|
||||
import { faPencil, faPlay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Badge } from '$lib/components/common'
|
||||
import { LanguageIcon } from '../common/languageIcons'
|
||||
|
||||
export let script: Script
|
||||
</script>
|
||||
@@ -17,15 +18,9 @@
|
||||
|
||||
<div class="inline-flex justify-between w-full">
|
||||
<div class="text-xs">{script.path}</div>
|
||||
|
||||
<Badge color="gray">
|
||||
{truncateHash(script.hash)}
|
||||
</Badge>
|
||||
<div><LanguageIcon height={16} lang={script.language} /></div>
|
||||
</div>
|
||||
<div class="inline-flex space-x-1 w-full">
|
||||
<Badge color="blue" capitalize>
|
||||
{script.language}
|
||||
</Badge>
|
||||
{#if script.kind !== 'script'}
|
||||
<Badge color="green" capitalize>
|
||||
{script.kind}
|
||||
|
||||
@@ -5,16 +5,7 @@
|
||||
import HatIcon from '../icons/HatIcon.svelte'
|
||||
</script>
|
||||
|
||||
<div class="p-4 border border-gray-300 rounded-md bg-gray-50 overflow-auto">
|
||||
<div class="flex items-center">
|
||||
<h3 class="inline-flex items-center font-semibold">
|
||||
<HatIcon class="w-6 h-6 mr-2" />
|
||||
Getting started
|
||||
</h3>
|
||||
</div>
|
||||
<div class="mt-2 mb-4 text-sm text-gray-700 ">
|
||||
Create a new script or find inspiration on the Hub!
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="inline-flex flex-wrap gap-y-2">
|
||||
<Button
|
||||
href="/scripts/add"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
]
|
||||
|
||||
const thirdMenuLinks = [
|
||||
{ label: 'Documentation', href: 'https://docs.windmill.dev/docs/intro/', icon: faBookOpen },
|
||||
{ label: 'Docs', href: 'https://docs.windmill.dev/docs/intro/', icon: faBookOpen },
|
||||
{ label: 'Feedbacks', href: 'https://discord.gg/V7PM2YHsPB', icon: faDiscord },
|
||||
{
|
||||
label: 'Issues',
|
||||
|
||||
@@ -59,9 +59,8 @@ export function displayDate(dateString: string | undefined): string {
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
return ''
|
||||
} else {
|
||||
return `${date.getFullYear()}/${
|
||||
date.getMonth() + 1
|
||||
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1
|
||||
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Badge, Skeleton } from '$lib/components/common'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { loadHubFlows } from '$lib/utils'
|
||||
|
||||
type Item = { apps: string[]; summary: string }
|
||||
let hubFlows: any[] | undefined = undefined
|
||||
let filteredItems: (Item & { marked?: string })[] = []
|
||||
let filter = ''
|
||||
let appFilter: string | undefined = undefined
|
||||
|
||||
$: prefilteredItems = appFilter
|
||||
? (hubFlows ?? []).filter((i) => i.apps.includes(appFilter))
|
||||
: hubFlows ?? []
|
||||
|
||||
$: apps = Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(async () => {
|
||||
hubFlows = await loadHubFlows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col min-h-0">
|
||||
<div class="w-12/12 pb-2 flex flex-row mt-1 gap-1">
|
||||
<input type="text" placeholder="Search Flows" bind:value={filter} class="text-2xl grow" />
|
||||
</div>
|
||||
|
||||
<div class="gap-2 w-full flex flex-wrap pb-2">
|
||||
{#each apps as app}
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
appFilter = appFilter == app ? undefined : app
|
||||
}}
|
||||
capitalize
|
||||
color={app === appFilter ? 'blue' : 'gray'}
|
||||
>
|
||||
{app}
|
||||
{#if app === appFilter}✗{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{#if hubFlows}
|
||||
{#if filter.length > 0 && filteredItems.length == 0}
|
||||
<p>No items found</p>
|
||||
{/if}
|
||||
{#each filteredItems as obj}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="py-4 px-1 gap-1 flex flex-row grow hover:bg-blue-50 bg-white transition-all"
|
||||
on:click={() => {
|
||||
dispatch('pick', obj)
|
||||
}}
|
||||
>
|
||||
<div class="mr-2 text-sm text-left w-32 shrink-0">{obj.apps.join(', ')}</div>
|
||||
<div class="mr-2 text-left">
|
||||
{#if obj.marked}
|
||||
{@html obj.marked ?? ''}
|
||||
{:else}
|
||||
{obj.summary ?? ''}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as sk}
|
||||
<Skeleton layout={[[4], 0.5]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
+187
-307
@@ -7,122 +7,59 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js'
|
||||
import { FlowService, type OpenFlow } from '$lib/gen'
|
||||
import type { Flow } from '$lib/gen'
|
||||
|
||||
import { sendUserToast, groupBy, canWrite, loadHubFlows } from '$lib/utils'
|
||||
import { sendUserToast, canWrite } from '$lib/utils'
|
||||
import {
|
||||
faArchive,
|
||||
faBuilding,
|
||||
faCalendarAlt,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
faEye,
|
||||
faGlobe,
|
||||
faList,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faShare
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import { Button, Tabs, Tab, Skeleton } from '../lib/components/common'
|
||||
import { Button, Tabs, Tab, Skeleton, Badge } from '../lib/components/common'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import CreateActions from '$lib/components/flows/CreateActions.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import PickHubFlow from './PickHubFlow.svelte'
|
||||
|
||||
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'hub'
|
||||
type Section = [string, FlowW[]]
|
||||
type FlowW = Flow & { canWrite: boolean; tab: Tab }
|
||||
type Tab = 'hub' | 'workspace'
|
||||
type FlowW = Flow & { canWrite: boolean; marked?: string }
|
||||
let flows: FlowW[] = []
|
||||
let filteredFlows: FlowW[]
|
||||
let hubFlows: any[] | undefined = undefined
|
||||
let filteredHubFlows: any[]
|
||||
let filteredFlows: FlowW[] = []
|
||||
let flowFilter = ''
|
||||
let groupedFlows: Section[] = []
|
||||
let hubFilter = ''
|
||||
let tab: Tab = 'all'
|
||||
let tab: Tab = 'workspace'
|
||||
let shareModal: ShareModal
|
||||
let loading = {
|
||||
general: true,
|
||||
hub: true
|
||||
}
|
||||
|
||||
const flowFuseOptions = {
|
||||
includeScore: false,
|
||||
keys: ['description', 'path', 'content', 'hash', 'summary']
|
||||
}
|
||||
const flowFuse: Fuse<FlowW> = new Fuse(flows, flowFuseOptions)
|
||||
|
||||
const flowHubFuse: Fuse<any> = new Fuse([], {
|
||||
includeScore: false,
|
||||
keys: ['summary']
|
||||
})
|
||||
|
||||
$: filteredFlows =
|
||||
flowFilter.length > 0 ? flowFuse.search(flowFilter).map((value) => value.item) : flows
|
||||
|
||||
$: filteredHubFlows =
|
||||
hubFilter.length > 0 ? flowHubFuse.search(hubFilter).map((value) => value.item) : hubFlows ?? []
|
||||
|
||||
let flowViewer: Drawer
|
||||
let flowViewerFlow: OpenFlow | undefined
|
||||
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
|
||||
|
||||
$: {
|
||||
let defaults: string[] = []
|
||||
|
||||
if (tab == 'all' || tab == 'personal') {
|
||||
defaults = defaults.concat(`u/${$userStore?.username}`)
|
||||
}
|
||||
if (tab == 'all' || tab == 'groups') {
|
||||
defaults = defaults.concat($userStore?.groups.map((x) => `g/${x}`) ?? [])
|
||||
}
|
||||
groupedFlows = groupBy(
|
||||
filteredFlows,
|
||||
(sc: Flow) => sc.path.split('/').slice(0, 2).join('/'),
|
||||
defaults
|
||||
)
|
||||
}
|
||||
|
||||
function tabFromPath(path: string) {
|
||||
let t: Tab = 'shared'
|
||||
let path_prefix = path.split('/').slice(0, 2)
|
||||
if (path_prefix[0] == 'u' && path_prefix[1] == $userStore?.username) {
|
||||
t = 'personal'
|
||||
} else if (path_prefix[0] == 'g' && $userStore?.groups.includes(path_prefix[1])) {
|
||||
t = 'groups'
|
||||
}
|
||||
return t
|
||||
}
|
||||
let loading = true
|
||||
|
||||
async function loadFlows(): Promise<void> {
|
||||
const allFlows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map(
|
||||
(x: Flow) => {
|
||||
let t: Tab = tabFromPath(x.path)
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
tab: t,
|
||||
...x
|
||||
}
|
||||
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
|
||||
return {
|
||||
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
...x
|
||||
}
|
||||
)
|
||||
flows = tab == 'all' ? allFlows : allFlows.filter((x) => x.tab == tab)
|
||||
loading.general = false
|
||||
flowFuse.setCollection(flows)
|
||||
}
|
||||
|
||||
async function loadHubFlowsWFuse(): Promise<void> {
|
||||
hubFlows = await loadHubFlows()
|
||||
loading.hub = false
|
||||
flowHubFuse.setCollection(hubFlows ?? [])
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function archiveFlow(path: string): Promise<void> {
|
||||
@@ -135,13 +72,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function viewFlow(id: number): Promise<void> {
|
||||
const hub = (await FlowService.getHubFlowById({ id: Number(id) })).flow
|
||||
async function viewFlow(obj: { flow_id: number }): Promise<void> {
|
||||
// console.log(obj)
|
||||
|
||||
const hub = await FlowService.getHubFlowById({ id: obj.flow_id })
|
||||
flowViewerFlow = hub
|
||||
flowViewer.openDrawer()
|
||||
}
|
||||
|
||||
loadHubFlowsWFuse()
|
||||
$: owners = Array.from(
|
||||
new Set(filteredFlows?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
$: preFilteredFlows =
|
||||
ownerFilter != undefined ? flows.filter((x) => x.path.startsWith(ownerFilter ?? '')) : flows
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && ($userStore || $superadmin)) {
|
||||
@@ -150,13 +96,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
filter={flowFilter}
|
||||
items={preFilteredFlows}
|
||||
bind:filteredItems={filteredFlows}
|
||||
f={(x) => x.summary + ' (' + x.path + ')'}
|
||||
/>
|
||||
|
||||
<Drawer bind:this={flowViewer} size="900px">
|
||||
<DrawerContent
|
||||
title="Hub flow '{flowViewerFlow?.summary ?? ''}'"
|
||||
on:close={flowViewer.closeDrawer}
|
||||
>
|
||||
{#if flowViewerFlow}
|
||||
<FlowViewer flow={flowViewerFlow} />
|
||||
<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>
|
||||
@@ -169,228 +129,148 @@
|
||||
</PageHeader>
|
||||
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab value="all">All</Tab>
|
||||
<Tab value="hub">Hub</Tab>
|
||||
<Tab value="personal">{`Personal space (${$userStore?.username})`}</Tab>
|
||||
<Tab value="groups">Groups</Tab>
|
||||
<Tab value="shared">Shared</Tab>
|
||||
<Tab size="xl" value="workspace"><Icon data={faBuilding} class="mr-1" /> Workspace</Tab>
|
||||
<Tab size="xl" value="hub"><Icon data={faGlobe} class="mr-1" /> Hub</Tab>
|
||||
</Tabs>
|
||||
|
||||
<div class="mb-1" />
|
||||
{#if tab != 'hub'}
|
||||
<input type="text" placeholder="Search flows" bind:value={flowFilter} class="search-bar mt-2" />
|
||||
{/if}
|
||||
<div class="grid grid-cols-1 divide-y">
|
||||
{#each tab == 'all' ? ['personal', 'groups', 'shared', 'hub'] : [tab] as sectionTab}
|
||||
<div class="shadow p-4 my-2">
|
||||
{#if sectionTab == 'personal'}
|
||||
<h2>
|
||||
<span class="text-lg xl:text-xl">Personal</span>
|
||||
<span class="text-sm">
|
||||
({`u/${$userStore?.username}`}) <Tooltip>
|
||||
All flows owned by you (and visible only to you if you do not explicitly share them)
|
||||
</Tooltip></span
|
||||
>
|
||||
</h2>
|
||||
{:else if sectionTab == 'groups'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Groups <Tooltip>All flows being owned by groups that you are member of</Tooltip>
|
||||
</h2>
|
||||
{:else if sectionTab == 'shared'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Shared <Tooltip>All flows visible to you because they have been shared to you</Tooltip>
|
||||
</h2>
|
||||
{:else if sectionTab == 'hub'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Approved flows from the WindmillHub <Tooltip>
|
||||
All approved Flow from the <a href="https://hub.windmill.dev">WindmillHub</a>.
|
||||
Approved flows have been potentially contributed by the community but reviewed and
|
||||
selected carefully by the Windmill team.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search hub flows"
|
||||
bind:value={hubFilter}
|
||||
class="search-bar mt-2"
|
||||
/>
|
||||
<div class="relative mt-2">
|
||||
{#if loading.hub}
|
||||
<Skeleton
|
||||
loading={loading.hub}
|
||||
layout={[0.4, [3], 0.4, [3], 0.4, [3], 0.4, [3], 0.4, [3], 0.4, [3], 0.4, [3]]}
|
||||
/>
|
||||
{:else if hubFlows != undefined}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Apps</th>
|
||||
<th>Summary</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each filteredHubFlows ?? [] as { summary, apps, flow_id }}
|
||||
<tr>
|
||||
<td class="font-black">{apps.join(', ')}</td>
|
||||
<td
|
||||
><button class="text-left" on:click={() => viewFlow(flow_id)}
|
||||
>{summary}</button
|
||||
></td
|
||||
>
|
||||
<td class="whitespace-nowrap"
|
||||
><button class="text-blue-500" on:click={() => viewFlow(flow_id)}
|
||||
>view flow</button
|
||||
>
|
||||
|
|
||||
<a target="_blank" href={`https://hub.windmill.dev/flows/${flow_id}`}
|
||||
>hub's page
|
||||
</a>
|
||||
| <a class="font-bold" href={`/flows/add?hub=${flow_id}`}>fork</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{:else}
|
||||
<span class="mt-2 text-sm text-red-400">
|
||||
Hub not reachable. If your environment is air gapped, contact sales@windmill.dev to
|
||||
setup a local mirror.
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#each groupedFlows.filter((x) => tabFromPath(x[0]) == sectionTab) as [section, flows]}
|
||||
{#if sectionTab != 'personal'}
|
||||
<div class="font-bold text-gray-700 mt-2 mb-2">
|
||||
{section}
|
||||
{#if section == 'g/all'}
|
||||
<Tooltip
|
||||
>'g/all' is the namespace for the group all. Every user is a member of all.
|
||||
Everything in this namespace is visible by all users. At the opposite, 'u/myuser'
|
||||
are private user namespaces.</Tooltip
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if loading.general}
|
||||
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 mt-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[7.6]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if flows.length == 0 && sectionTab == 'personal'}
|
||||
<p class="text-xs text-gray-600 italic mt-2">No flows yet</p>
|
||||
{:else}
|
||||
<div class="grid md:grid-cols-2 gap-4 sm:grid-cols-1 xl:grid-cols-3 mt-2">
|
||||
{#each flows as { summary, path, extra_perms, canWrite }}
|
||||
<a
|
||||
class="border border-gray-400 p-4 rounded-sm shadow-sm space-y-2 hover:border-blue-600 text-gray-800 flex flex-col justify-between"
|
||||
href="/flows/get/{path}"
|
||||
>
|
||||
<div class="px-6 overflow-auto ">
|
||||
<div class="font-semibold text-gray-700">
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
</div>
|
||||
<p class="text-gray-700 text-xs">
|
||||
<a class="text-gray-700 text-xs" href="/flows/get/{path}">{path} </a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row pl-6 pr-2 mt-2">
|
||||
<div class="mr-3 w-full">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
<div class="flex flex-row-reverse w-full place space-x-1">
|
||||
<div>
|
||||
<Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'View flow',
|
||||
icon: faEye,
|
||||
href: `/flows/get/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
href: `/flows/edit/${path}`,
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Use as template/Fork',
|
||||
icon: faCodeFork,
|
||||
href: `/flows/add?template=${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'View runs',
|
||||
icon: faList,
|
||||
href: `/runs/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
icon: faCalendarAlt,
|
||||
href: `/schedule/add?path=${path}&isFlow=true`
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
},
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Archive',
|
||||
icon: faArchive,
|
||||
action: () => {
|
||||
path ? archiveFlow(path) : null
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
href="/flows/edit/{path}"
|
||||
endIcon={{ icon: faEdit }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
href="/flows/add?template={path}"
|
||||
endIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<input type="text" placeholder="Search Flows" bind:value={flowFilter} class="text-2xl mt-2" />
|
||||
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
href="/flows/run/{path}"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2 w-full flex flex-wrap pb-1 pt-2">
|
||||
{#each owners as owner}
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
ownerFilter = ownerFilter == owner ? undefined : owner
|
||||
}}
|
||||
color={owner === ownerFilter ? 'blue' : 'gray'}
|
||||
>
|
||||
{owner}
|
||||
{#if owner === ownerFilter}✗{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if tab == 'workspace'}
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mt-2 w-full">
|
||||
{#if !loading}
|
||||
{#each filteredFlows as { summary, path, extra_perms, canWrite, marked }}
|
||||
<a
|
||||
class="border border-gray-400 p-2 rounded-sm shadow-sm hover:border-blue-600 text-gray-800 flex flex-row items-center justify-between"
|
||||
href="/flows/get/{path}"
|
||||
>
|
||||
<div class="flex flex-col gap-1 w-full h-full">
|
||||
<div class="font-semibold text-gray-700 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full grow gap-2 items-start">
|
||||
<div class="text-gray-700 text-xs flex flex-row flex-wrap gap-x-1 items-center"
|
||||
>{path}
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
<div class="flex flex-row-reverse place gap-x-2 pt-4">
|
||||
<div>
|
||||
<Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'View flow',
|
||||
icon: faEye,
|
||||
href: `/flows/get/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
href: `/flows/edit/${path}`,
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Use as template/Fork',
|
||||
icon: faCodeFork,
|
||||
href: `/flows/add?template=${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'View runs',
|
||||
icon: faList,
|
||||
href: `/runs/${path}`
|
||||
},
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
icon: faCalendarAlt,
|
||||
href: `/schedule/add?path=${path}&isFlow=true`
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
},
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Archive',
|
||||
icon: faArchive,
|
||||
action: () => {
|
||||
path ? archiveFlow(path) : null
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
href="/flows/run/{path}"
|
||||
startIcon={{ icon: faPlay }}>Run</Button
|
||||
>
|
||||
</div>
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
href="/flows/edit/{path}"
|
||||
startIcon={{ icon: faEdit }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
href="/flows/add?template={path}"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div></div
|
||||
></a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as sk}
|
||||
<Skeleton layout={[[4]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<PickHubFlow on:pick={(e) => viewFlow(e.detail)} />
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
<ShareModal
|
||||
|
||||
+109
-208
@@ -9,6 +9,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
faArchive,
|
||||
faBuilding,
|
||||
faCalendarAlt,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
@@ -18,108 +19,55 @@
|
||||
faPlay,
|
||||
faShare
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Fuse from 'fuse.js'
|
||||
import type { Script } from '$lib/gen'
|
||||
import { ScriptService } from '$lib/gen'
|
||||
import { superadmin, userStore, workspaceStore, hubScripts } from '$lib/stores'
|
||||
import {
|
||||
canWrite,
|
||||
getScriptByPath,
|
||||
groupBy,
|
||||
loadHubScripts,
|
||||
sendUserToast,
|
||||
truncateHash
|
||||
} from '$lib/utils'
|
||||
import { superadmin, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, getScriptByPath, sendUserToast } from '$lib/utils'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import { Button, Tabs, Tab, Badge, Skeleton, DrawerContent } from '$lib/components/common'
|
||||
import CreateActions from '$lib/components/scripts/CreateActions.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import type { HubItem } from '$lib/components/flows/pickers/model'
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'examples' | 'hub'
|
||||
type Section = [string, ScriptW[]]
|
||||
type ScriptW = Script & { canWrite: boolean; tab: Tab }
|
||||
type Tab = 'workspace' | 'hub'
|
||||
type ScriptW = Script & { canWrite: boolean; marked?: string }
|
||||
let scripts: ScriptW[] = []
|
||||
let filteredScripts: ScriptW[]
|
||||
let scriptFilter = ''
|
||||
let hubFilter = ''
|
||||
let groupedScripts: Section[] = []
|
||||
let communityScripts: Section[] = []
|
||||
let preFilteredScripts: ScriptW[] = []
|
||||
let filteredScripts: ScriptW[] = []
|
||||
let filter = ''
|
||||
|
||||
let loading = true
|
||||
|
||||
let tab: Tab = 'all'
|
||||
let tab: Tab = 'workspace'
|
||||
|
||||
let shareModal: ShareModal
|
||||
|
||||
const fuseOptions = {
|
||||
includeScore: false,
|
||||
keys: ['description', 'path', 'content', 'hash', 'summary']
|
||||
}
|
||||
const fuse: Fuse<ScriptW> = new Fuse(scripts, fuseOptions)
|
||||
|
||||
const hubScriptsFuse: Fuse<any> = new Fuse($hubScripts ?? [], {
|
||||
includeScore: false,
|
||||
keys: ['app', 'path', 'summary']
|
||||
})
|
||||
|
||||
let codeViewer: Drawer
|
||||
let codeViewerContent: string = ''
|
||||
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
|
||||
let codeViewerObj: HubItem | undefined = undefined
|
||||
|
||||
$: filteredScripts =
|
||||
scriptFilter.length > 0 ? fuse.search(scriptFilter).map((value) => value.item) : scripts
|
||||
|
||||
$: {
|
||||
let defaults: string[] = []
|
||||
|
||||
if (tab == 'all' || tab == 'personal') {
|
||||
defaults = defaults.concat(`u/${$userStore?.username}`)
|
||||
}
|
||||
if (tab == 'all' || tab == 'groups') {
|
||||
defaults = defaults.concat($userStore?.groups.map((x) => `g/${x}`) ?? [])
|
||||
}
|
||||
groupedScripts = groupBy(
|
||||
filteredScripts.filter((x) => x.tab != 'examples'),
|
||||
(sc: Script) => sc.path.split('/').slice(0, 2).join('/'),
|
||||
defaults
|
||||
)
|
||||
communityScripts = [['examples', filteredScripts.filter((x) => x.tab == 'examples')]]
|
||||
}
|
||||
|
||||
function tabFromPath(path: string) {
|
||||
let t: Tab = 'shared'
|
||||
let path_prefix = path.split('/').slice(0, 2)
|
||||
if (path_prefix[0] == 'u' && path_prefix[1] == $userStore?.username) {
|
||||
t = 'personal'
|
||||
} else if (path_prefix[0] == 'g' && $userStore?.groups.includes(path_prefix[1])) {
|
||||
t = 'groups'
|
||||
}
|
||||
return t
|
||||
}
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const allScripts = (
|
||||
await ScriptService.listScripts({ workspace: $workspaceStore!, perPage: 300 })
|
||||
).map((x: Script) => {
|
||||
let t: Tab = x.workspace_id == $workspaceStore ? tabFromPath(x.path) : 'examples'
|
||||
return {
|
||||
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
tab: t,
|
||||
...x
|
||||
scripts = (await ScriptService.listScripts({ workspace: $workspaceStore!, perPage: 300 })).map(
|
||||
(x: Script) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
...x
|
||||
}
|
||||
}
|
||||
})
|
||||
scripts = tab == 'all' ? allScripts : allScripts.filter((x) => x.tab == tab)
|
||||
)
|
||||
loading = false
|
||||
fuse.setCollection(scripts)
|
||||
}
|
||||
|
||||
async function archiveScript(path: string): Promise<void> {
|
||||
@@ -136,20 +84,29 @@
|
||||
codeViewer.openDrawer()
|
||||
}
|
||||
|
||||
async function loadHubScriptsWFuse(): Promise<void> {
|
||||
await loadHubScripts()
|
||||
hubScriptsFuse.setCollection($hubScripts ?? [])
|
||||
}
|
||||
$: owners = Array.from(
|
||||
new Set(filteredScripts?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
loadHubScriptsWFuse()
|
||||
$: preFilteredScripts =
|
||||
ownerFilter != undefined ? scripts.filter((x) => x.path.startsWith(ownerFilter ?? '')) : scripts
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && ($userStore || $superadmin) && tab) {
|
||||
if ($workspaceStore && ($userStore || $superadmin)) {
|
||||
loadScripts()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={preFilteredScripts}
|
||||
bind:filteredItems={filteredScripts}
|
||||
f={(x) => x.summary + ' (' + x.path + ')'}
|
||||
/>
|
||||
|
||||
<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"
|
||||
@@ -179,122 +136,58 @@
|
||||
</PageHeader>
|
||||
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab value="all">All</Tab>
|
||||
<Tab value="hub">Hub</Tab>
|
||||
<Tab value="personal">{`Personal space (${$userStore?.username})`}</Tab>
|
||||
<Tab value="groups">Groups</Tab>
|
||||
<Tab value="shared">Shared</Tab>
|
||||
<Tab value="examples">Examples</Tab>
|
||||
<Tab size="xl" value="workspace"><Icon data={faBuilding} class="mr-1" /> Workspace</Tab>
|
||||
<Tab size="xl" value="hub"><Icon data={faGlobe} class="mr-1" /> Hub</Tab>
|
||||
</Tabs>
|
||||
|
||||
{#if tab != 'hub'}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search scripts"
|
||||
bind:value={scriptFilter}
|
||||
class="search-bar mt-2"
|
||||
/>
|
||||
{/if}
|
||||
<div class="mb-1" />
|
||||
|
||||
<div class="grid grid-cols-1 divide-y">
|
||||
{#each tab == 'all' ? ['personal', 'groups', 'shared', 'examples', 'hub'] : [tab] as sectionTab}
|
||||
<div class="shadow p-4 my-2">
|
||||
{#if sectionTab == 'personal'}
|
||||
<h2>
|
||||
<span class="text-lg xl:text-xl">Personal</span>
|
||||
<span class="text-sm">
|
||||
({`u/${$userStore?.username}`}) <Tooltip>
|
||||
All scripts owned by you (and visible only to you if you do not explicitly share
|
||||
them)
|
||||
</Tooltip>
|
||||
</span>
|
||||
</h2>
|
||||
{:else if sectionTab == 'groups'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Groups <Tooltip>All scripts being owned by groups that you are member of</Tooltip>
|
||||
</h2>
|
||||
{:else if sectionTab == 'shared'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Shared <Tooltip>All scripts visible to you because they have been shared to you</Tooltip
|
||||
>
|
||||
</h2>
|
||||
{:else if sectionTab == 'examples'}
|
||||
<h2 class="text-lg xl:text-xl">
|
||||
Public <Tooltip>
|
||||
Template and examples shared across all workspaces of this instance. They are managed
|
||||
from a special workspace called 'starter' that only superadmin can change.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
{:else if sectionTab == 'hub'}
|
||||
<h2 class="text-lg xl:text-xl mb-4">
|
||||
Approved scripts from the WindmillHub <Tooltip>
|
||||
All approved Deno scripts from the <a href="https://hub.windmill.dev">WindmillHub</a>.
|
||||
Approved scripts have been reviewed by the Windmill team and are safe to use in
|
||||
production.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
{#if tab == 'workspace'}
|
||||
<input type="text" placeholder="Search Scripts" bind:value={filter} class="text-2xl mt-2" />
|
||||
|
||||
<div class="flex flex-col max-h-screen">
|
||||
{#if $hubScripts != undefined}
|
||||
<PickHubScript on:pick={(e) => viewCode(e.detail)} />
|
||||
{:else}
|
||||
<span class="mt-2 text-sm">
|
||||
Hub not reachable. If your environment is air gapped, contact sales@windmill.dev to
|
||||
setup a local mirror.
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#each sectionTab == 'examples' ? communityScripts : groupedScripts.filter((x) => tabFromPath(x[0]) == sectionTab) as [section, scripts]}
|
||||
{#if sectionTab != 'personal' && sectionTab != 'examples'}
|
||||
<div class="font-bold text-gray-700 mt-2 mb-2">
|
||||
{section}
|
||||
{#if section == 'g/all'}
|
||||
<Tooltip
|
||||
>'g/all' is the namespace for the group all. Every user is a member of all.
|
||||
Everything in this namespace is visible by all users. At the opposite, 'u/myuser'
|
||||
are private user namespaces.</Tooltip
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 mt-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[8.5]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if scripts.length == 0 && sectionTab == 'personal'}
|
||||
<p class="text-xs text-gray-600 italic mt-2">No scripts yet</p>
|
||||
{:else}
|
||||
<div class="grid md:grid-cols-2 gap-4 sm:grid-cols-1 xl:grid-cols-3 mt-2">
|
||||
{#each scripts as { summary, path, hash, language, extra_perms, canWrite, lock_error_logs, kind }}
|
||||
<a
|
||||
class="border border-gray-400 p-4 rounded-sm shadow-sm space-y-2 hover:border-blue-600 text-gray-800 flex flex-col justify-between"
|
||||
href="/scripts/get/{hash}"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a href="/scripts/get/{hash}" class="px-6">
|
||||
<div class="font-semibold text-gray-700">
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
</div>
|
||||
<p class="text-gray-700 text-xs">
|
||||
{path}
|
||||
<Badge color="gray" baseClass="text-xs">{truncateHash(hash)}</Badge>
|
||||
</p>
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-1 px-6">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
<Badge color="blue" capitalize>{language}</Badge>
|
||||
{#if kind != 'script'}
|
||||
<Badge color="blue" capitalize>{kind}</Badge>
|
||||
{/if}
|
||||
{#if lock_error_logs}
|
||||
<Badge color="red">Deployment error</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse items-end w-full gap-2 pr-2 mt-2">
|
||||
<div class="gap-2 w-full flex flex-wrap pb-1 pt-2">
|
||||
{#each owners as owner}
|
||||
<Badge
|
||||
class="cursor-pointer hover:bg-gray-200"
|
||||
on:click={() => {
|
||||
ownerFilter = ownerFilter == owner ? undefined : owner
|
||||
}}
|
||||
color={owner === ownerFilter ? 'blue' : 'gray'}
|
||||
>
|
||||
{owner}
|
||||
{#if owner === ownerFilter}✗{/if}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mt-2 w-full">
|
||||
{#if !loading}
|
||||
{#each filteredScripts as { summary, path, hash, language, extra_perms, canWrite, lock_error_logs, kind, marked } (path)}
|
||||
<a
|
||||
class="border border-gray-400 p-2 rounded-sm shadow-sm hover:border-blue-600 text-gray-800"
|
||||
href="/scripts/get/{hash}"
|
||||
>
|
||||
<div class="flex flex-col gap-1 w-full h-full">
|
||||
<div class="font-semibold text-gray-700 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full grow gap-2 items-start">
|
||||
<div class="text-gray-700 text-xs flex flex-row flex-wrap gap-x-1 items-center">
|
||||
{path}
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
<div><LanguageIcon height={16} lang={language} /></div>
|
||||
{#if kind != 'script'}
|
||||
<Badge color="blue" capitalize>{kind}</Badge>
|
||||
{/if}
|
||||
{#if lock_error_logs}
|
||||
<Badge color="red">Deployment error</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col items-end grow pt-4">
|
||||
<div class="flex flex-row-reverse place gap-x-2 items-end">
|
||||
<div>
|
||||
<Dropdown
|
||||
dropdownItems={[
|
||||
@@ -350,10 +243,21 @@
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: faPlay }}
|
||||
href="/scripts/run/{hash}"
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="dark"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}?step=2"
|
||||
@@ -364,6 +268,7 @@
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
@@ -373,25 +278,21 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
startIcon={{ icon: faPlay }}
|
||||
href="/scripts/run/{hash}"
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div></div
|
||||
>
|
||||
</div></a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as sk}
|
||||
<Skeleton layout={[[4]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<PickHubScript on:pick={(e) => viewCode(e.detail)} />
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
<ShareModal
|
||||
|
||||
@@ -176,6 +176,11 @@ const config = {
|
||||
fontSize: '25px'
|
||||
}
|
||||
},
|
||||
mark: {
|
||||
backgroundColor: theme('colors.yellow.200'),
|
||||
border: `1px solid ${theme('colors.gray.600')}`,
|
||||
borderRadius: theme('borderRadius.sm'),
|
||||
},
|
||||
h3: {
|
||||
fontSize: '18px',
|
||||
fontWeight: theme('fontWeight.bold'),
|
||||
|
||||
Reference in New Issue
Block a user