feat(frontend): deeper integration with the hub

This commit is contained in:
Ruben Fiszel
2022-07-23 21:11:31 +02:00
parent ba4de1af0a
commit bb58eba2b5
12 changed files with 190 additions and 94 deletions

View File

@@ -17,6 +17,10 @@
a {
@apply text-blue-500;
}
.flex pre code.hljs {
@apply text-sm;
}
#table-custom th {
@apply py-3.5 text-left text-sm font-semibold text-gray-900 capitalize;

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { ResourceService, ScriptService, VariableService } from '$lib/gen'
import { sendUserToast } from '$lib/utils'
import { getScriptByPath, loadHubScripts, sendUserToast } from '$lib/utils'
import Icon from 'svelte-awesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { workspaceStore } from '$lib/stores'
import { workspaceStore, hubScripts } from '$lib/stores'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import ResourceEditor from './ResourceEditor.svelte'
@@ -26,6 +26,7 @@
let resourceEditor: ResourceEditor
let codeViewer: Modal
let codeLang: 'python3' | 'deno' = 'deno'
let codeContent: string = ''
async function loadVariables() {
@@ -44,7 +45,13 @@
}
async function loadScripts(): Promise<{ path: string; summary?: string }[]> {
return await ScriptService.listScripts({ workspace: $workspaceStore ?? 'NO_W' })
const workspaceScripts: { path: string; summary?: string }[] = await ScriptService.listScripts({
workspace: $workspaceStore ?? 'NO_W'
})
await loadHubScripts()
const hubScripts_ = $hubScripts ?? []
return workspaceScripts.concat(hubScripts_)
}
</script>
@@ -55,12 +62,9 @@
<ItemPicker
bind:this={scriptPicker}
pickCallback={async (path, _) => {
codeContent = (
await ScriptService.getScriptByPath({
workspace: $workspaceStore ?? '',
path
})
).content
const { language, content } = await getScriptByPath(path ?? '')
codeContent = content
codeLang = language
codeViewer.openModal()
}}
closeOnClick={false}
@@ -72,9 +76,9 @@
<Modal bind:this={codeViewer}>
<div slot="title">Code</div>
<div slot="content">
{#if lang == 'python3'}
{#if codeLang == 'python3'}
<Highlight language={python} code={codeContent} />
{:else if lang == 'deno'}
{:else if codeLang == 'deno'}
<Highlight language={typescript} code={codeContent} />
{/if}
</div></Modal

View File

@@ -3,7 +3,7 @@
import { page } from '$app/stores'
import { FlowService, ScriptService, type Flow } from '$lib/gen'
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
import { sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import { loadHubScripts, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import { faFileExport, faFileImport } from '@fortawesome/free-solid-svg-icons'
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
@@ -20,16 +20,6 @@
$: step = Number($page.url.searchParams.get('step')) || 1
async function loadSearchData() {
const scripts = await ScriptService.listHubScripts()
$hubScripts = scripts.map((x) => ({
path: `hub/${x.id}/${x.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: `${x.summary} (${x.app})`,
approved: x.approved,
is_trigger: x.is_trigger
}))
}
async function saveFlow(): Promise<void> {
const newFlow = flowToMode($flowStore, mode)
@@ -70,7 +60,7 @@
})
onMount(() => {
loadSearchData()
loadHubScripts()
clearPreviewResults()
})
</script>

View File

@@ -12,6 +12,7 @@
import { python, typescript } from 'svelte-highlight/languages'
import github from 'svelte-highlight/styles/github'
import { getScript, getScriptByPath } from '$lib/utils'
export let scriptPath: string | undefined = undefined
export let allowFlow = false
@@ -30,20 +31,6 @@
allowFlow && options.push(['Flow', 'flow'])
const dispatch = createEventDispatcher()
async function getScript() {
if (itemKind == 'hub') {
code = await ScriptService.getHubScriptContentByPath({ path: scriptPath! })
lang = Script.language.DENO
} else {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: scriptPath!
})
code = script.content
lang = script.language
}
}
async function loadItems(): Promise<void> {
if (itemKind == 'flow') {
items = await FlowService.listFlows({ workspace: $workspaceStore! })
@@ -92,7 +79,9 @@
<button
class="text-xs text-blue-500"
on:click={async () => {
await getScript()
const { language, content } = await getScriptByPath(scriptPath ?? '')
code = content
lang = language
modalViewer.openModal()
}}>show code</button
>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { FlowModuleValue, Script, type FlowModule } from '$lib/gen'
import { getScriptByPath } from '$lib/utils'
import { faCode, faCodeBranch, faSave, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Highlight } from 'svelte-highlight'
@@ -7,7 +8,7 @@
import github from 'svelte-highlight/styles/github'
import Modal from '../Modal.svelte'
import { createScriptFromInlineScript, fork, removeModule } from './flowStore'
import { getScriptByPath, scrollIntoView } from './utils'
import { scrollIntoView } from './utils'
export let open: number
export let i: number
@@ -16,7 +17,7 @@
let modalViewer: Modal
let modalViewerContent = ''
let modalViewerLanguage: Script.language = Script.language.DENO
let modalViewerLanguage: 'deno' | 'python3' = 'deno'
async function viewCode() {
const { content, language } = await getScriptByPath(mod.value.path!)

View File

@@ -9,7 +9,7 @@ import {
import { inferArgs } from '$lib/infer'
import { loadSchema } from '$lib/scripts'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { emptySchema, getScriptByPath } from '$lib/utils'
import { get } from 'svelte/store'
import type { FlowMode } from './flowStore'
@@ -79,34 +79,14 @@ export async function getFirstStepSchema(flow: Flow): Promise<Schema> {
return emptySchema()
}
export async function getScriptByPath(path: string): Promise<{
content: string
language: FlowModuleValue.language
}> {
if (path.startsWith('hub/')) {
const content = await ScriptService.getHubScriptContentByPath({ path })
return {
content,
language: FlowModuleValue.language.DENO
}
} else {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path: path ?? ''
})
return {
content: script.content,
language: script.language
}
}
}
export async function createInlineScriptModuleFromPath(path: string): Promise<FlowModuleValue> {
const { content, language } = await getScriptByPath(path)
return {
type: FlowModuleValue.type.RAWSCRIPT,
language: language,
language: language as FlowModuleValue.language,
content: content,
path
}

View File

@@ -27,7 +27,8 @@ export const hubScripts = writable<
path: string
summary: string
approved: boolean
is_trigger: boolean
is_trigger: boolean,
app: string
}>
| undefined
>(undefined)

View File

@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { goto } from '$app/navigation'
import type { User } from '$lib/gen'
import { Script, ScriptService, type User } from '$lib/gen'
import { toast } from '@zerodevx/svelte-toast'
import { get } from 'svelte/store'
import type { Schema } from './common'
import type { UserExt } from './stores'
import { hubScripts, workspaceStore, type UserExt } from './stores'
@@ -405,8 +406,43 @@ export function setInputCat(type: string | undefined, format: string | undefined
export function scriptPathToHref(path: string): string {
if (path.startsWith('hub/')) {
return 'https://hub.windmill.dev/scripts/get/' + path.substring(4)
return 'https://hub.windmill.dev/from_version/' + path.substring(4)
} else {
return `/scripts/get/${path}`
}
}
export async function getScriptByPath(path: string): Promise<{
content: string
language: 'deno' | 'python3',
}> {
if (path.startsWith('hub/')) {
const content = await ScriptService.getHubScriptContentByPath({ path })
return {
content,
language: 'deno'
}
} else {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path: path ?? ''
})
return {
content: script.content,
language: script.language
}
}
}
export async function loadHubScripts() {
const scripts = await ScriptService.listHubScripts()
hubScripts.set(scripts.map((x) => ({
path: `hub/${x.id}/${x.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
summary: `${x.summary} (${x.app})`,
approved: x.approved,
is_trigger: x.is_trigger,
app: x.app
})))
}

View File

@@ -25,7 +25,8 @@
'Model not found',
'Connection is disposed.',
'Connection got disposed.',
'Stopping the server timed out'
'Stopping the server timed out',
'Canceled'
]
async function loadUser() {

View File

@@ -2,7 +2,7 @@
import { page } from '$app/stores'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { addModule, initFlow } from '$lib/components/flows/flowStore'
import { initFlow } from '$lib/components/flows/flowStore'
import type { Flow } from '$lib/gen'
import { emptySchema } from '$lib/utils'

View File

@@ -13,8 +13,8 @@
import Icon from 'svelte-awesome'
import type { Script } from '$lib/gen'
import { ScriptService } from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { canWrite, groupBy, sendUserToast, truncateHash } from '$lib/utils'
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'
@@ -24,13 +24,18 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import Tabs from '$lib/components/Tabs.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'
import github from 'svelte-highlight/styles/github'
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'community'
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[] = []
@@ -50,9 +55,23 @@
const templateFuse: Fuse<Script> = new Fuse(templateScripts, 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 ?? []
$: filteredTemplates =
templateFilter.length > 0
? templateFuse.search(templateFilter).map((value) => value.item)
@@ -68,11 +87,11 @@
defaults = defaults.concat($userStore?.groups.map((x) => `g/${x}`) ?? [])
}
groupedScripts = groupBy(
filteredScripts.filter((x) => x.tab != 'community'),
filteredScripts.filter((x) => x.tab != 'examples'),
(sc: Script) => sc.path.split('/').slice(0, 2).join('/'),
defaults
)
communityScripts = [['community', filteredScripts.filter((x) => x.tab == 'community')]]
communityScripts = [['examples', filteredScripts.filter((x) => x.tab == 'examples')]]
}
async function loadTemplateScripts(): Promise<void> {
@@ -97,7 +116,7 @@
async function loadScripts(): Promise<void> {
const allScripts = (await ScriptService.listScripts({ workspace: $workspaceStore! })).map(
(x: Script) => {
let t: Tab = x.workspace_id == $workspaceStore ? tabFromPath(x.path) : 'community'
let t: Tab = x.workspace_id == $workspaceStore ? tabFromPath(x.path) : 'examples'
return {
canWrite:
canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
@@ -116,19 +135,36 @@
sendUserToast(`Successfully archived script ${path}`)
}
async function viewCode(path) {
codeViewerContent = (await getScriptByPath(path)).content
codeViewerPath = path
codeViewer.openModal()
}
$: {
if ($workspaceStore && ($userStore || $superadmin)) {
loadScripts()
loadHubScripts()
}
}
</script>
<svelte:head>
{@html github}
</svelte:head>
<Modal bind:this={codeViewer}>
<div slot="title">{codeViewerPath}</div>
<div slot="content">
<Highlight language={typescript} code={codeViewerContent} />
</div></Modal
>
<CenteredPage>
<PageHeader
title="Scripts"
tooltip="Scripts are the building blocks of windmill. A script 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
tooltip="Scripts are the building blocks of windmill. 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...'. L
ike 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."
@@ -153,14 +189,17 @@
['personal', `personal space (${$userStore?.username})`],
['groups', 'groups'],
['shared', 'shared'],
['community', 'community']
['examples', 'examples'],
['hub', 'hub']
]}
bind:tab
on:update={loadScripts}
/>
<input placeholder="Search scripts" bind:value={scriptFilter} class="search-bar mt-2" />
{#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', 'community'] : [tab] as sectionTab}
{#each tab == 'all' ? ['personal', 'groups', 'shared', 'examples', 'hub'] : [tab] as sectionTab}
<div class="shadow p-4 my-2">
{#if sectionTab == 'personal'}
<h2 class="">
@@ -168,32 +207,71 @@
</h2>
<p class="italic text-xs text-gray-600 mb-4">
All scripts owned by you (and visible only to you if you do not explicitely share them)
will be displayed below
</p>
{:else if sectionTab == 'groups'}
<h2 class="">Groups that I am member of</h2>
<p class="italic text-xs text-gray-600">
All scripts being owned by groups that you are member of will be displayed below
All scripts being owned by groups that you are member of
</p>
{:else if sectionTab == 'shared'}
<h2 class="">Shared with me</h2>
<p class="italic text-xs text-gray-600">
All scripts visible to you because they have been shared to you will be displayed below
All scripts visible to you because they have been shared to you
</p>
{:else if sectionTab == 'community'}
<h2 class="">Community templates & examples</h2>
{:else if sectionTab == 'examples'}
<h2 class="">Shared across all workspaces of this instance</h2>
<p class="italic text-xs text-gray-600 mb-8">
All scripts by the community that went through a review process and merged to the
<a href="https://github.com/windmill-labs/windmill">official github repo</a> will be displayed
below. Contributions welcome as Github PR.
Template and examples shared across all workspaces of this instance. They are managed
from a special workspace called 'starter' that only superadmin can change.
</p>
{:else if sectionTab == 'hub'}
<h2 class="">Approved scripts from the WindmillHub</h2>
<p class="italic text-xs text-gray-600 mb-8">
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.
</p>
<input placeholder="Search hub scripts" bind:value={hubFilter} class="search-bar mt-2" />
<div class="relative">
<TableCustom>
<tr slot="header-row">
<th>App</th>
<th>Summary</th>
<th />
</tr>
<tbody slot="body">
{#each filteredHub ?? [] as { path, summary, app }}
<tr>
<td class="font-black">{app}</td>
<td><button on:click={() => viewCode(path)}>{summary}</button></td>
<td
><button class="text-blue-500" on:click={() => viewCode(path)}
>view code</button
>
|
<a
target="_blank"
href={`https://hub.windmill.dev/from_version/${path
.split('/')
.slice(1, 3)
.join('/')}`}>hub's page</a
>
| <a href={`/scripts/add?hub=${encodeURIComponent(path)}`}>fork</a>
</td>
</tr>
{/each}
</tbody>
</TableCustom>
</div>
{/if}
{#each sectionTab == 'community' ? communityScripts : groupedScripts.filter((x) => tabFromPath(x[0]) == sectionTab) as [section, scripts]}
{#if sectionTab != 'personal' && sectionTab != 'community'}
{#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">
owner: {section}
{#if section == 'g/all'}
<Tooltip class="mx-1"
<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

View File

@@ -1,16 +1,17 @@
<script lang="ts">
import { ScriptService, type Script } from '$lib/gen'
import { Script, ScriptService } from '$lib/gen'
import { page } from '$app/stores'
import { workspaceStore } from '$lib/stores'
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import type { Schema } from '$lib/common'
import { emptySchema, sendUserToast } from '$lib/utils'
import { emptySchema, getScriptByPath, sendUserToast } from '$lib/utils'
// Default
let schema: Schema = emptySchema()
$: templatePath = $page.url.searchParams.get('template')
$: hubPath = $page.url.searchParams.get('hub')
const initialState = $page.url.searchParams.get('state')
@@ -47,9 +48,20 @@
}
}
async function loadHub(): Promise<void> {
if (hubPath) {
const template = await getScriptByPath(hubPath)
script.summary = `Fork of ${hubPath}`
script.content = template.content
script.language = Script.language.DENO
sendUserToast(`Code has been loaded from hub script ${hubPath}.`)
}
}
$: {
if ($workspaceStore) {
loadTemplate()
loadHub()
}
}
</script>