mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
cc07a6b7e4
* branches * iterate * branches * progress * progress * progress * done * adapt frontend * adapt frontend * sqlx
431 lines
13 KiB
Svelte
431 lines
13 KiB
Svelte
<script context="module">
|
|
export function load() {
|
|
return {
|
|
stuff: { title: 'Scripts' }
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
faArchive,
|
|
faCalendarAlt,
|
|
faCodeFork,
|
|
faEdit,
|
|
faEye,
|
|
faList,
|
|
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 Badge from '$lib/components/Badge.svelte'
|
|
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
|
import Dropdown from '$lib/components/Dropdown.svelte'
|
|
import Modal from '$lib/components/Modal.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 { Highlight } from 'svelte-highlight'
|
|
import { typescript } from 'svelte-highlight/languages/typescript'
|
|
import { Button } from '$lib/components/common'
|
|
import CreateActions from '$lib/components/scripts/CreateActions.svelte'
|
|
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
|
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
|
|
|
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'examples' | 'hub'
|
|
type Section = [string, ScriptW[]]
|
|
type ScriptW = Script & { canWrite: boolean; tab: Tab }
|
|
let scripts: ScriptW[] = []
|
|
let filteredScripts: ScriptW[]
|
|
let scriptFilter = ''
|
|
let hubFilter = ''
|
|
let groupedScripts: Section[] = []
|
|
let communityScripts: Section[] = []
|
|
|
|
let tab: Tab = 'all'
|
|
|
|
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: Modal
|
|
let codeViewerContent: string = ''
|
|
let codeViewerPath: string = ''
|
|
|
|
$: filteredScripts =
|
|
scriptFilter.length > 0 ? fuse.search(scriptFilter).map((value) => value.item) : scripts
|
|
|
|
$: filteredHub =
|
|
hubFilter.length > 0
|
|
? hubScriptsFuse.search(hubFilter).map((value) => value.item)
|
|
: $hubScripts ?? []
|
|
|
|
$: {
|
|
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
|
|
}
|
|
|
|
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 = tab == 'all' ? allScripts : allScripts.filter((x) => x.tab == tab)
|
|
fuse.setCollection(scripts)
|
|
}
|
|
|
|
async function archiveScript(path: string): Promise<void> {
|
|
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
|
|
loadScripts()
|
|
sendUserToast(`Successfully archived script ${path}`)
|
|
}
|
|
|
|
async function viewCode(path: string) {
|
|
codeViewerContent = (await getScriptByPath(path)).content
|
|
codeViewerPath = path
|
|
codeViewer.openModal()
|
|
}
|
|
|
|
async function loadHubScriptsWFuse(): Promise<void> {
|
|
await loadHubScripts()
|
|
hubScriptsFuse.setCollection($hubScripts ?? [])
|
|
}
|
|
|
|
loadHubScriptsWFuse()
|
|
|
|
$: {
|
|
if ($workspaceStore && ($userStore || $superadmin) && tab) {
|
|
loadScripts()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Modal bind:this={codeViewer}>
|
|
<div slot="title">{codeViewerPath}</div>
|
|
<div slot="content">
|
|
<Highlight language={typescript} code={codeViewerContent} />
|
|
</div>
|
|
</Modal>
|
|
|
|
<CenteredPage>
|
|
<PageHeader
|
|
title="Scripts"
|
|
tooltip="A script can either be used standalone or as part of a Flow.
|
|
When standalone, it has an auto-generated UI from its parameters whom you can access clicking on 'Run'.
|
|
Like everything in windmill, scripts have owners (users or groups) and can be shared to other users and other groups. It is enough to have
|
|
read-access on a script to be able to execute it. However, you will also need to have been
|
|
granted visibility on the resources and variables it uses, otherwise it will behave as if those
|
|
items did not exist at runtime of the script."
|
|
>
|
|
<CreateActions />
|
|
</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>
|
|
</Tabs>
|
|
|
|
{#if tab != 'hub'}
|
|
<input placeholder="Search scripts" bind:value={scriptFilter} class="search-bar mt-2" />
|
|
{/if}
|
|
|
|
<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 class="mb-2">
|
|
Personal <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="">
|
|
Groups <Tooltip>All scripts being owned by groups that you are member of</Tooltip>
|
|
</h2>
|
|
{:else if sectionTab == 'shared'}
|
|
<h2 class="">
|
|
Shared <Tooltip>All scripts visible to you because they have been shared to you</Tooltip
|
|
>
|
|
</h2>
|
|
{:else if sectionTab == 'examples'}
|
|
<h2 class="">
|
|
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="">
|
|
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. The hub only offers Deno scripts because Hub scripts are meant to be
|
|
solely used as building blocks of flows and are much more efficient to execute than
|
|
their Python counterparts.
|
|
</Tooltip>
|
|
</h2>
|
|
<input placeholder="Search hub scripts" bind:value={hubFilter} class="search-bar mt-2" />
|
|
<div class="relative">
|
|
{#if $hubScripts != undefined}
|
|
<TableCustom>
|
|
<tr slot="header-row">
|
|
<th>App</th>
|
|
<th>Summary</th>
|
|
<th />
|
|
</tr>
|
|
<tbody slot="body">
|
|
{#each filteredHub ?? [] as { path, summary, app, ask_id }}
|
|
<tr>
|
|
<td class="font-black">{app}</td>
|
|
<td
|
|
><button class="text-left" on:click={() => viewCode(path)}>{summary}</button
|
|
></td
|
|
>
|
|
<td class="whitespace-nowrap"
|
|
><button class="text-blue-500" on:click={() => viewCode(path)}
|
|
>view code</button
|
|
>
|
|
|
|
|
<a
|
|
target="_blank"
|
|
href={`https://hub.windmill.dev/scripts/${app}/${ask_id}`}>hub's page</a
|
|
>
|
|
|
|
|
<a class="font-bold" href={`/scripts/add?hub=${encodeURIComponent(path)}`}
|
|
>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 sectionTab == 'examples' ? communityScripts : groupedScripts.filter((x) => tabFromPath(x[0]) == sectionTab) as [section, scripts]}
|
|
{#if sectionTab != 'personal' && sectionTab != 'examples'}
|
|
<h3 class="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}
|
|
</h3>
|
|
{/if}
|
|
{#if scripts.length == 0 && sectionTab == 'personal'}
|
|
<p class="text-xs text-gray-600 italic">No scripts yet</p>
|
|
{:else}
|
|
<div class="grid md:grid-cols-2 gap-4 sm:grid-cols-1 xl:grid-cols-3">
|
|
{#each scripts as { summary, path, hash, language, extra_perms, canWrite, lock_error_logs, kind }}
|
|
<div
|
|
class="flex flex-col justify-between script max-w-lg overflow-visible shadow-sm shadow-blue-100 border border-gray-200 bg-gray-50 py-2"
|
|
>
|
|
<a href="/scripts/get/{hash}">
|
|
<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="/scripts/get/{hash}">{path} </a><span
|
|
class="commit-hash ml-3">{truncateHash(hash)}</span
|
|
>
|
|
</p>
|
|
</div>
|
|
</a>
|
|
<div class="flex flex-row pl-6 pr-2 mt-2">
|
|
<div class="mr-3 w-full">
|
|
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
|
<Badge twBgColor="bg-blue-200">{language}</Badge>
|
|
{#if kind != 'script'}
|
|
<Badge twBgColor="bg-blue-300">{kind}</Badge>
|
|
{/if}
|
|
{#if lock_error_logs}<Badge
|
|
twBgColor="bg-red-200"
|
|
tooltip="The script was not deployed due to an error during deployment. See more details about the error on the script page."
|
|
>Deployment error</Badge
|
|
>{/if}
|
|
</div>
|
|
<div class="flex flex-row-reverse w-full place space-x-1">
|
|
<div>
|
|
<Dropdown
|
|
dropdownItems={[
|
|
{
|
|
displayName: 'View script',
|
|
icon: faEye,
|
|
href: `/scripts/get/${hash}`
|
|
},
|
|
{
|
|
displayName: 'Edit',
|
|
icon: faEdit,
|
|
href: `/scripts/edit/${hash}`,
|
|
disabled: !canWrite
|
|
},
|
|
{
|
|
displayName: 'Edit code',
|
|
icon: faEdit,
|
|
href: `/scripts/edit/${hash}?step=2`,
|
|
disabled: !canWrite
|
|
},
|
|
{
|
|
displayName: 'Use as template',
|
|
icon: faCodeFork,
|
|
href: `/scripts/add?template=${path}`
|
|
},
|
|
{
|
|
displayName: 'View runs',
|
|
icon: faList,
|
|
href: `/runs/${path}`
|
|
},
|
|
{
|
|
displayName: 'Schedule',
|
|
icon: faCalendarAlt,
|
|
href: `/schedule/add?path=${path}`
|
|
},
|
|
{
|
|
displayName: 'Share',
|
|
icon: faShare,
|
|
action: () => {
|
|
shareModal.openModal(path)
|
|
},
|
|
disabled: !canWrite
|
|
},
|
|
{
|
|
displayName: 'Archive',
|
|
icon: faArchive,
|
|
action: () => {
|
|
path ? archiveScript(path) : null
|
|
},
|
|
type: 'delete',
|
|
disabled: !canWrite
|
|
}
|
|
]}
|
|
/>
|
|
</div>
|
|
{#if canWrite}
|
|
<div>
|
|
<Button
|
|
variant="border"
|
|
size="xs"
|
|
startIcon={{ icon: faEdit }}
|
|
href="/scripts/edit/{hash}?step=2"
|
|
>
|
|
Edit
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
<div>
|
|
<Button
|
|
variant="border"
|
|
size="xs"
|
|
startIcon={{ icon: faCodeFork }}
|
|
href="/scripts/add?template={path}"
|
|
>
|
|
Fork
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
<div>
|
|
<Button
|
|
variant="border"
|
|
size="xs"
|
|
startIcon={{ icon: faPlay }}
|
|
href="/scripts/run/{hash}"
|
|
>
|
|
Run
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</CenteredPage>
|
|
|
|
<ShareModal
|
|
bind:this={shareModal}
|
|
kind="script"
|
|
on:change={() => {
|
|
loadScripts()
|
|
}}
|
|
/>
|
|
|
|
<style>
|
|
.selected:hover {
|
|
@apply border border-gray-500 rounded-md border-opacity-50;
|
|
}
|
|
|
|
.script:hover {
|
|
@apply border border-gray-600 border-opacity-60;
|
|
}
|
|
</style>
|