UI refactor (#973)

* fix(frontend): ui refactor

* fix(frontend): WIP

* fix(frontend): WIP

* fix(frontend): WIP

* fix(frontend): remove virtual table

* fix(frontend): Add virtual list

* fix(frontend): fix number of items

* fix(frontend): revert changes

* fix(frontend): fix build
This commit is contained in:
Faton Ramadani
2022-12-02 19:39:23 +01:00
committed by GitHub
parent dfaa9fd63a
commit d4c1e1ab78
42 changed files with 886 additions and 784 deletions
+9 -1
View File
@@ -4831,7 +4831,15 @@ components:
execution_mode:
type: string
enum: [viewer, publisher, anonymous]
required:
- id
- workspace_id
- path
- summary
- version
- extra_perms
- edited_at
- execution_mode
AppWithLastVersion:
type: object
properties:
+11
View File
@@ -12,6 +12,7 @@
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@leeoniya/ufuzzy": "^0.8.0",
"@redocly/json-to-json-schema": "^0.0.1",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@types/node": "^18.11.9",
"async-mutex": "^0.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
@@ -483,6 +484,11 @@
"vite": "^3.0.0"
}
},
"node_modules/@sveltejs/svelte-virtual-list": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz",
"integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ=="
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.1.tgz",
@@ -7341,6 +7347,11 @@
"tiny-glob": "^0.2.9"
}
},
"@sveltejs/svelte-virtual-list": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz",
"integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ=="
},
"@sveltejs/vite-plugin-svelte": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.1.tgz",
+1
View File
@@ -63,6 +63,7 @@
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@leeoniya/ufuzzy": "^0.8.0",
"@redocly/json-to-json-schema": "^0.0.1",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@types/node": "^18.11.9",
"async-mutex": "^0.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
+12
View File
@@ -22,6 +22,17 @@
.monaco-workbench > .notifications-toasts.visible {
display: none !important;
}
svelte-virtual-list-row {
overflow: visible !important;
}
svelte-virtual-list-contents > * + * {
border-top-width: 1px !important;
border-bottom-width: 0px !important
}
}
@layer components {
@@ -31,3 +42,4 @@
cursor: pointer !important;
}
}
+12 -6
View File
@@ -78,7 +78,8 @@
scale={0.6}
class="inline mr-2 {item.type == 'delete' ? 'text-red-500' : 'text-gray-700'}"
/>
{/if}{item.displayName}
{/if}
{item.displayName}
</button>
{:else if item.href}
<a
@@ -93,21 +94,26 @@
tabindex="-1"
id="user-menu-item-{name}-{i}}"
class:disabled={item.disabled}
>{#if item.icon}
>
{#if item.icon}
<Icon
data={item.icon}
scale={0.6}
class="inline mr-2 {item.type == 'delete' ? 'text-red-500' : 'text-gray-700'}"
/>
{/if}{item.displayName}</a
>
{/if}
{item.displayName}
</a>
{:else}
<span
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}">{item.displayName}</span
>{/if}
id="user-menu-item-{name}-{i}}"
>
{item.displayName}
</span>
{/if}
{/each}
{/if}
</div>
-153
View File
@@ -1,153 +0,0 @@
<script lang="ts">
import { FlowService, ScriptService, type Flow, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
faCodeFork,
faEdit,
faEye,
faList,
faPlay,
faShare,
faWind
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { Button } from './common'
import Dropdown from './Dropdown.svelte'
import SharedBadge from './SharedBadge.svelte'
import type ShareModal from './ShareModal.svelte'
import Star from './Star.svelte'
export let flow: Flow & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
const { summary, path, extra_perms, canWrite, workspace_id } = flow
async function archiveFlow(path: string): Promise<void> {
try {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived flow ${path}`)
} catch (err) {
sendUserToast(`Could not archive this flow ${err.body}`, true)
}
}
const dispatch = createEventDispatcher()
</script>
<a
class="border border-gray-400 py-2 px-4 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">
<Icon data={faWind} class="mr-2" scale={1} />
{#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}
<Star
workspace_id={workspace_id ?? $workspaceStore ?? ''}
kind="flow"
{path}
{starred}
on:starred={() => dispatch('change')}
/>
<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>
<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
>
+5 -5
View File
@@ -6,11 +6,11 @@
export let args: any
</script>
<TableCustom class="px-10 py-2">
<tr slot="header-row"
><th>Argument</th>
<th>Value</th></tr
>
<TableCustom class="py-2">
<tr slot="header-row">
<th>Argument</th>
<th>Value</th>
</tr>
<tbody slot="body">
{#if args && Object.keys(args).length > 0}
{#each Object.entries(args) as [arg, value]}
@@ -9,14 +9,14 @@
<div class="flex flex-row flex-wrap justify-between mt-4 mb-2 min-h-[48px]">
{#if primary}
<span class="flex items-center space-x-2">
<h1 class="mr-0.5">{title}</h1>
<h1>{title}</h1>
{#if tooltip}
<Tooltip>{tooltip}</Tooltip>
{/if}
</span>
{:else}
<span class="flex items-center space-x-2">
<h2 class="mr-0.5">{title}</h2>
<h2>{title}</h2>
{#if tooltip}
<Tooltip>{tooltip}</Tooltip>
{/if}
@@ -1,173 +0,0 @@
<script lang="ts">
import { ScriptService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
faCodeFork,
faEdit,
faEye,
faList,
faPlay,
faScroll,
faShare
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { Badge, Button } from './common'
import { LanguageIcon } from './common/languageIcons'
import Dropdown from './Dropdown.svelte'
import SharedBadge from './SharedBadge.svelte'
import type ShareModal from './ShareModal.svelte'
import Star from './Star.svelte'
export let script: Script & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
let {
summary,
path,
hash,
language,
extra_perms,
canWrite,
lock_error_logs,
kind,
workspace_id
} = script
const dispatch = createEventDispatcher()
async function archiveScript(path: string): Promise<void> {
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived script ${path}`)
}
</script>
<a
class="border border-gray-400 py-2 px-4 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">
<Icon data={faScroll} class="mr-2" scale={1} />
{#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}
<Star
kind="script"
{path}
{starred}
workspace_id={workspace_id ?? $workspaceStore ?? ''}
on:starred={() => dispatch('change')}
/>
<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">
<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.openDrawer(path)
},
disabled: !canWrite
},
{
displayName: 'Archive',
icon: faArchive,
action: () => {
path ? archiveScript(path) : null
},
type: 'delete',
disabled: !canWrite
}
]}
/>
</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"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="dark"
variant="border"
size="xs"
startIcon={{ icon: faCodeFork }}
href="/scripts/add?template={path}"
>
Fork
</Button>
</div>
{/if}
</div>
</div></div
>
</div></a
>
@@ -167,7 +167,7 @@
<Pane size={40} minSize={10}>
<Splitpanes horizontal>
<Pane size={33}>
<div class="w-full bg-gray-100 px-2 text-sm">Preview</div>
<div class="w-full border-b p-2 text-xs font-bold">Preview</div>
<div class="px-2">
<div class="break-words relative font-sans">
<SchemaForm compact {schema} bind:args bind:isValid />
@@ -49,7 +49,7 @@
</script>
{#if kind === 'read' || kind === 'write'}
<Badge icon={{ data: faPeopleGroup }} capitalize color="blue">
<Badge icon={{ data: faPeopleGroup }} capitalize color="blue" baseClass="border border-blue-200">
{kind}
{#if reason}
<Tooltip>{reason}</Tooltip>
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { FavoriteService } from '$lib/gen'
import { starStore, workspaceStore } from '$lib/stores'
import { starStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { faStar } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
@@ -0,0 +1,73 @@
<script lang="ts">
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { AppService, Policy } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { Button, Drawer, DrawerContent } from '$lib/components/common'
let path: string = ''
let initialPath = ''
let pathError = ''
import type { App } from '$lib/components/apps/types'
import { goto } from '$app/navigation'
import Path from '$lib/components/Path.svelte'
let drawerOpen = false
function closeDrawer() {
drawerOpen = false
}
async function createApp() {
const appJson: App = {
grid: [],
title: 'New app',
inlineScripts: {}
}
const policy = {
triggerables: {},
execution_mode: Policy.execution_mode.PUBLISHER,
on_behalf_of: `u/${$userStore?.username}`
}
try {
const appId = await AppService.createApp({
workspace: $workspaceStore!,
requestBody: {
value: appJson,
path,
summary: 'App summary',
policy
}
})
goto(`/apps/edit/${appId}`)
} catch (e) {
sendUserToast('Error creating app', e)
}
}
</script>
<Drawer bind:open={drawerOpen} size="800px">
<DrawerContent title="Add an app" on:close={() => closeDrawer()}>
<Path bind:error={pathError} bind:path {initialPath} namePlaceholder="my_app" kind="app">
<div slot="ownerToolkit">
App permissions depend on their path. Select the group <span class="font-mono">all</span>
to share it, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<Button on:click={() => createApp()}>Create app</Button>
</DrawerContent>
</Drawer>
<Button
size="sm"
spacingSize="lg"
startIcon={{ icon: faPlus }}
on:click={() => (drawerOpen = true)}
>
New app
</Button>
@@ -3,6 +3,9 @@
export let value: string
export let debounceDelay: number = 500
let parentClass: string | undefined = undefined
export { parentClass as class }
let timer: NodeJS.Timeout
function debounce(event: KeyboardEvent): void {
@@ -15,4 +18,4 @@
}
</script>
<input {placeholder} on:keyup={debounce} />
<input {placeholder} on:keyup={debounce} class={parentClass} />
@@ -8,9 +8,8 @@
export let large = false
export let href = ''
export let rounded = false
export let index = false
export let dismissable = false
export let baseClass = 'text-center -mb-0.5'
export let baseClass = 'text-center'
export let capitalize = false
export let icon: BadgeIconProps | undefined = undefined
@@ -51,10 +50,6 @@
href &&
(color.startsWith(ColorModifier) ? hovers[color.replace(ColorModifier, '')] : hovers[color]),
rounded ? 'rounded-full px-2 py-1' : 'rounded px-2.5 py-0.5',
index
? 'absolute flex justify-center items-center font-bold overflow-hidden border-2 border-white dark:border-gray-900' +
(large ? 'w-7 h-7 -top-3.5 -right-3.5' : 'w-6 h-6 -top-3 -right-3')
: '',
$$props.class
)
$: iconProps = icon ? { ...defaulIconProps, ...icon } : { data: undefined }
@@ -14,6 +14,7 @@
export let target: ButtonType.Target = '_self'
export let startIcon: ButtonType.Icon | undefined = undefined
export let endIcon: ButtonType.Icon | undefined = undefined
export let spacingSize: ButtonType.Size = size
let ref: ButtonType.Element
@@ -24,7 +25,8 @@
size,
color,
variant,
disabled
disabled,
spacingSize
}
</script>
@@ -0,0 +1,65 @@
<script lang="ts">
import Dropdown from '$lib/components/Dropdown.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import type { ListableApp } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { faEdit, faEye } from '@fortawesome/free-solid-svg-icons'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
export let app: ListableApp & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
let { summary, path, extra_perms, canWrite, workspace_id } = app
</script>
<Row
href={`/apps/get/${path}`}
kind="app"
{marked}
{path}
{summary}
workspaceId={workspace_id ?? $workspaceStore ?? ''}
{starred}
on:change
>
<svelte:fragment slot="badges">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Dropdown
dropdownItems={[
{
displayName: 'View app',
icon: faEye,
href: `/apps/get/${path}`
}
]}
/>
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/apps/edit/{path}"
>
Edit
</Button>
</div>
{/if}
<Button
href="/apps/get/{path}"
color="dark"
size="xs"
spacingSize="md"
startIcon={{ icon: faEye }}
>
View
</Button>
</svelte:fragment>
</Row>
@@ -0,0 +1,152 @@
<script lang="ts">
import Dropdown from '$lib/components/Dropdown.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import type ShareModal from '$lib/components/ShareModal.svelte'
import { FlowService, type Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
faCodeFork,
faEdit,
faEye,
faList,
faPlay,
faShare
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
export let flow: Flow & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
let { summary, path, extra_perms, canWrite, workspace_id } = flow
const dispatch = createEventDispatcher()
async function archiveFlow(path: string): Promise<void> {
try {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived flow ${path}`)
} catch (err) {
sendUserToast(`Could not archive this flow ${err.body}`, true)
}
}
</script>
<Row
href={`/flows/get/${path}`}
kind="flow"
workspaceId={workspace_id ?? $workspaceStore ?? ''}
{marked}
{path}
{summary}
{starred}
on:change
>
<svelte:fragment slot="badges">
<SharedBadge {canWrite} extraPerms={extra_perms} />
</svelte:fragment>
<svelte:fragment slot="actions">
<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 && shareModal.openDrawer(path)
},
disabled: !canWrite
},
{
displayName: 'Archive',
icon: faArchive,
action: () => {
path ? archiveFlow(path) : null
},
type: 'delete',
disabled: !canWrite
}
]}
/>
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/flows/edit/{path}"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/flows/add?template={path}"
>
Fork
</Button>
</div>
{/if}
<Button
href="/flows/get/{path}"
color="light"
variant="border"
size="xs"
spacingSize="md"
startIcon={{ icon: faEye }}
>
Detail
</Button>
<Button
href="/flows/run/{path}"
color="dark"
size="xs"
spacingSize="md"
endIcon={{ icon: faPlay }}
>
Run
</Button>
</svelte:fragment>
</Row>
@@ -0,0 +1,53 @@
<script lang="ts">
import Star from '$lib/components/Star.svelte'
import { createEventDispatcher } from 'svelte'
import RowIcon from './RowIcon.svelte'
export let marked: string | undefined
export let starred: boolean
const dispatch = createEventDispatcher()
export let kind: 'script' | 'flow' | 'app' = 'script'
export let summary: string | undefined = undefined
export let path: string
export let href: string
export let workspaceId: string
</script>
<a class="hover:bg-gray-50 cursor-pointer w-full flex items-center p-4 gap-4" {href}>
<RowIcon {kind} />
<div class="w-full">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if marked}
{@html marked}
{:else}
{!summary || summary.length == 0 ? path : summary}
{/if}
</div>
<div class="text-gray-600 text-xs ">
{path}
</div>
</div>
<div class="w-96 flex flex-row max-w-xs gap-1 items-start flex-wrap">
<slot name="badges" />
</div>
<div class="flex gap-1 items-center justify-end">
<slot name="actions" />
</div>
<div class="text-left text-sm font-semibold text-gray-900">
<Star
{kind}
{path}
{starred}
workspace_id={workspaceId}
on:starred={() => {
dispatch('change')
}}
/>
</div>
</a>
@@ -0,0 +1,28 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { Code2, LayoutDashboard, Wind } from 'svelte-lucide'
export let kind: 'script' | 'flow' | 'app'
const icon = {
script: Code2,
flow: Wind,
app: LayoutDashboard
}[kind]
const color = {
script: 'bg-blue-50 border-blue-200',
flow: 'bg-[#f0fdfa] border-[#99f6e4]',
app: 'bg-[#fff7ed] border-orange-200'
}[kind]
const iconColor = {
script: '#60A5FA',
flow: '#14b8a6',
app: '#fb923c'
}[kind]
</script>
<div class={classNames('rounded-md p-1 flex justify-center items-center border', color)}>
<svelte:component this={icon} size="18px" color={iconColor} />
</div>
@@ -0,0 +1,167 @@
<script lang="ts">
import Dropdown from '$lib/components/Dropdown.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import type ShareModal from '$lib/components/ShareModal.svelte'
import { ScriptService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { capitalize, sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
faCodeFork,
faEdit,
faEye,
faList,
faPlay,
faShare
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Badge from '../badge/Badge.svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
export let script: Script & { canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
let {
summary,
path,
hash,
language,
extra_perms,
canWrite,
lock_error_logs,
kind,
workspace_id
} = script
const dispatch = createEventDispatcher()
async function archiveScript(path: string): Promise<void> {
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Successfully archived script ${path}`)
}
</script>
<Row
href={`/scripts/get/${hash}`}
kind="script"
{marked}
{path}
{summary}
workspaceId={workspace_id ?? $workspaceStore ?? ''}
{starred}
>
<svelte:fragment slot="badges">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if lock_error_logs}
<Badge color="red" baseClass="border border-red-200">Deployment failed</Badge>
{/if}
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
</svelte:fragment>
<svelte:fragment slot="actions">
<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.openDrawer && shareModal.openDrawer(path)
},
disabled: !canWrite
},
{
displayName: 'Archive',
icon: faArchive,
action: () => {
path ? archiveScript(path) : null
},
type: 'delete',
disabled: !canWrite
}
]}
/>
{#if canWrite}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faEdit }}
href="/scripts/edit/{hash}?step=2"
>
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/scripts/add?template={path}"
>
Fork
</Button>
</div>
{/if}
<Button
href="/scripts/get/{hash}"
color="light"
variant="border"
size="xs"
spacingSize="md"
startIcon={{ icon: faEye }}
>
Detail
</Button>
<Button
href="/scripts/run/{hash}"
color="dark"
size="xs"
spacingSize="md"
endIcon={{ icon: faPlay }}
>
Run
</Button>
</svelte:fragment>
</Row>
@@ -0,0 +1,9 @@
<div class="flex flex-col">
<div class="overflow-hidden rounded-md border">
<table class="min-w-full divide-y divide-gray-300">
<tbody class="divide-y divide-gray-200 bg-white">
<slot />
</tbody>
</table>
</div>
</div>
@@ -17,10 +17,10 @@
btnClasses={classNames(
'border-gray-200 focus:ring-0 w-full',
position === 'left' ? 'rounded-none rounded-l-lg border' : '',
position === 'center' ? 'rounded-none border-t border-b' : '',
position === 'right' ? 'rounded-none rounded-r-md !border border-l-0' : ''
position === 'center' ? 'rounded-none border-t border-b border-r' : '',
position === 'right' ? 'rounded-none rounded-r-md border-r border-y' : ''
)}
color={$selected === value ? (light ? 'gray' : 'dark') : 'light'}
color={$selected === value ? (light ? 'dark' : 'dark') : 'light'}
variant="contained"
>
<slot />
@@ -31,6 +31,6 @@
})
</script>
<div class:flex-col={col} class="flex rounded-md shadow-sm" role="group">
<div class:flex-col={col} class="flex rounded-md" role="group">
<slot />
</div>
@@ -1,16 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { faPlus, faWind } from '@fortawesome/free-solid-svg-icons'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { loadHubFlows, sendUserToast } from '$lib/utils'
import { loadHubFlows } from '$lib/utils'
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
import ItemPicker from '../ItemPicker.svelte'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { flowStore, initFlow } from '$lib/components/flows/flowStore'
import Icon from 'svelte-awesome'
let hubFlows: any[] | undefined = undefined
@@ -21,7 +20,7 @@
hub: undefined,
json: undefined
}
let hubItems: any[] = []
let pendingJson: string
let hubFilter = ''
@@ -49,8 +48,8 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup size="md" startIcon={{ icon: faPlus }} href="/flows/add">
<svelte:fragment slot="main">New Flow<Icon data={faWind} class="ml-1" /></svelte:fragment>
<ButtonPopup size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/flows/add">
<svelte:fragment slot="main">New Flow</svelte:fragment>
<ButtonPopupItem on:click={() => drawers.json?.toggleDrawer()}>
Import from raw JSON
</ButtonPopupItem>
@@ -65,7 +65,7 @@
</DrawerContent>
</Drawer>
<Tabs bind:selected={selectedTab}>
<Tabs bind:selected={selectedTab} class="mt-1">
<Tab value="logs" size="xs">Logs/Result</Tab>
<Tab value="history" size="xs">History</Tab>
<Tab value="last_save" size="xs">Last save</Tab>
@@ -6,7 +6,6 @@
import { ScriptService } from '$lib/gen'
import { workspaceStore, hubScripts } from '$lib/stores'
import { ButtonPopup, ButtonPopupItem } from '$lib/components/common'
import ItemPicker from '../ItemPicker.svelte'
import type { HubItem } from '../flows/pickers/model'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
@@ -17,6 +16,7 @@
} = {
template: undefined
}
let hubItems: HubItem[]
let templateScripts: Script[] = []
let templateFilter = ''
@@ -45,8 +45,8 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup size="md" startIcon={{ icon: faPlus }} href="/scripts/add">
<svelte:fragment slot="main">New Script<Icon data={faScroll} class="ml-1" /></svelte:fragment>
<ButtonPopup size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/scripts/add">
<svelte:fragment slot="main">New Script</svelte:fragment>
<ButtonPopupItem on:click={() => drawers.template?.toggleDrawer()}>
Import from template
</ButtonPopupItem>
@@ -55,7 +55,7 @@
<nav class="h-full flex justify-between flex-col px-2">
<div class="space-y-1 pt-4">
{#each mainMenuLinks as menuLink (menuLink.href)}
<MenuLink class="text-lg" {...menuLink} {isCollapsed} />
<MenuLink class="text-md" {...menuLink} {isCollapsed} />
{/each}
<div class="h-2" />
<div class="max-h-40 overflow-y-auto max-w-xs truncate">
+4 -3
View File
@@ -42,7 +42,7 @@ export function displayDaysAgo(dateString: string): string {
} else {
let dAgo = daysAgo(date)
if (dAgo == 0) {
return `yday at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
return `yesterday at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
} else if (dAgo > 7) {
return `${dAgo + 1} days ago at ${date.toLocaleTimeString([], {
hour: '2-digit',
@@ -59,8 +59,9 @@ 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' })}`
}
}
-224
View File
@@ -1,224 +0,0 @@
<script context="module">
export function load() {
return {
stuff: { title: 'Scripts' }
}
}
</script>
<script lang="ts">
import { faCodeFork, faEdit, faPlay, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { AppService, Policy, type ListableApp } from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { canWrite, sendUserToast } from '$lib/utils'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import { Button, Badge, Skeleton, Drawer, DrawerContent } from '$lib/components/common'
type ListableAppMarked = ListableApp & { canWrite: boolean; marked?: string }
let apps: ListableAppMarked[] = []
let preFilteredApps: ListableAppMarked[] = []
let filteredApps: ListableAppMarked[] = []
let filter = ''
let loading = true
let path: string = ''
let initialPath = ''
let pathError = ''
async function loadApps(): Promise<void> {
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((x) => {
return {
canWrite:
x.path && x.extra_perms
? canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore
: false,
...x
}
})
loading = false
}
import SearchItems from '$lib/components/SearchItems.svelte'
import type { App } from '$lib/components/apps/types'
import { goto } from '$app/navigation'
import Path from '$lib/components/Path.svelte'
$: owners = Array.from(
new Set(filteredApps?.map((x) => x.path?.split('/').slice(0, 2).join('/')) ?? [])
).sort()
$: preFilteredApps =
ownerFilter != undefined ? apps.filter((x) => x.path?.startsWith(ownerFilter ?? '')) : apps
let ownerFilter: string | undefined = undefined
$: {
if ($workspaceStore && ($userStore || $superadmin)) {
loadApps()
}
}
async function createApp() {
const appJson: App = {
grid: [],
title: 'New app',
inlineScripts: {}
}
const policy = {
triggerables: {},
execution_mode: Policy.execution_mode.PUBLISHER,
on_behalf_of: `u/${$userStore?.username}`
}
try {
const appId = await AppService.createApp({
workspace: $workspaceStore!,
requestBody: {
value: appJson,
path,
summary: 'App summary',
policy
}
})
goto(`/apps/edit/${appId}`)
} catch (e) {
sendUserToast('Error creating app', e)
}
}
let drawerOpen = false
function closeDrawer() {
drawerOpen = false
}
</script>
<Drawer bind:open={drawerOpen} size="800px">
<DrawerContent title="Add an app" on:close={() => closeDrawer()}>
<Path bind:error={pathError} bind:path {initialPath} namePlaceholder="my_app" kind="app">
<div slot="ownerToolkit">
App permissions depend on their path. Select the group <span class="font-mono">all</span>
to share it, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<Button on:click={() => createApp()}>Create app</Button>
</DrawerContent>
</Drawer>
<SearchItems
{filter}
items={preFilteredApps}
bind:filteredItems={filteredApps}
f={(x) => x.summary + ' (' + x.path + ')'}
/>
<CenteredPage>
<PageHeader
title="Apps"
tooltip="A Script can be used standalone or as part of a Flow.
When standalone, it has webhooks and an auto-generated UI from its parameters whom you can access clicking on 'Run'.
Scripts have owners (users or groups) and can be shared to users and groups."
>
<Button size="md" startIcon={{ icon: faPlus }} on:click={() => (drawerOpen = true)}>
New app
</Button>
</PageHeader>
<div class="mb-1" />
<input type="text" placeholder="Search Scripts" bind:value={filter} class="text-2xl 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}&cross;{/if}
</Badge>
{/each}
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mt-2 w-full">
{#if !loading}
{#each filteredApps as { summary, path, extra_perms, marked, canWrite }, index (`${path}-${index}`)}
<a
class="border border-gray-400 p-2 rounded-sm shadow-sm hover:border-blue-600 text-gray-800"
href="/apps/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-col items-end grow pt-4">
<div class="flex flex-row-reverse place gap-x-2 items-end">
<div>
<Button
color="dark"
size="xs"
startIcon={{ icon: faPlay }}
href="/apps/get/{path}"
>
Preview
</Button>
</div>
{#if canWrite}
<div>
<Button
variant="border"
color="dark"
size="xs"
startIcon={{ icon: faEdit }}
href="/apps/edit/{path}"
>
Edit
</Button>
<Button
variant="border"
color="red"
size="xs"
startIcon={{ icon: faTrash }}
on:click={() => {
if ($workspaceStore && path) {
AppService.deleteApp({
workspace: $workspaceStore,
path
})
}
}}
>
Delete
</Button>
</div>
{/if}
</div>
</div>
</div>
</div>
</a>
{/each}
{:else}
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4]]} />
{/each}
{/if}
</div>
</CenteredPage>
@@ -4,8 +4,6 @@
import { workspaceStore } from '$lib/stores'
import { page } from '$app/stores'
import gridHelp from 'svelte-grid/build/helper/index.mjs'
let app: AppWithLastVersion | undefined = undefined
let path = $page.params.path
@@ -15,7 +15,7 @@
import Button from '$lib/components/common/button/Button.svelte'
import { AppService, AppWithLastVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { faArrowLeft, faPen } from '@fortawesome/free-solid-svg-icons'
import { faPen } from '@fortawesome/free-solid-svg-icons'
let app: AppWithLastVersion | undefined = undefined
@@ -33,12 +33,8 @@
<CenteredPage>
{#if app}
<div class="flex justify-between my-2 items-center">
<div class="flex gap-4 items-center">
<Button size="xs" href="/apps" startIcon={{ icon: faArrowLeft }} color="dark">
Back to apps
</Button>
<div>{app.value.title}</div>
</div>
<div>{app.value.title}</div>
<Button size="xs" href="/apps/edit/{$page.params.path}" startIcon={{ icon: faPen }}>
Edit
</Button>
+3 -3
View File
@@ -141,7 +141,7 @@
{#if logs}
{#each logs as { id, timestamp, username, operation, action_kind, resource, parameters }}
<tr>
<td class="">{id}</td>
<td>{id}</td>
<td class="">
<div class="whitespace-nowrap overflow-x-auto no-scrollbar max-w-xs">
{displayDate(timestamp)}
@@ -153,8 +153,8 @@
<td class="">
<div class="whitespace-nowrap overflow-x-auto no-scrollbar w-20">
{username}
</div></td
>
</div>
</td>
<td class=""><pre>{operation}</pre></td>
<td class="">{resource}</td>
<td class="">
@@ -34,9 +34,7 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
import Dropdown from '$lib/components/Dropdown.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import { Button, ActionRow, Skeleton, Badge } from '$lib/components/common'
+223 -146
View File
@@ -2,15 +2,18 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import JobDetail from '$lib/components/jobs/JobDetail.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,
@@ -25,16 +28,15 @@
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, getScriptByPath, sendUserToast } from '$lib/utils'
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 Icon from 'svelte-awesome'
import {
faBuilding,
faCodeFork,
faDashboard,
faGlobe,
faScroll,
faWind
faLayerGroup,
faPlus
} from '@fortawesome/free-solid-svg-icons'
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import PickHubFlow from './PickHubFlow.svelte'
@@ -42,20 +44,40 @@
import HighlightCode from '$lib/components/HighlightCode.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import ScriptBox from '$lib/components/ScriptBox.svelte'
import FlowBox from '$lib/components/FlowBox.svelte'
import type uFuzzy from '@leeoniya/ufuzzy'
import { Building, Code2, Globe2, LayoutDashboard, Wind } from 'svelte-lucide'
import Table from '$lib/components/common/table/Table.svelte'
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[] = []
type Tab = 'hubscripts' | 'hubflows' | 'workspace'
type ScriptW = Script & { canWrite: boolean; marked?: string }
type FlowW = Flow & { canWrite: boolean; marked?: string }
let scripts: ScriptW[] = []
let flows: FlowW[] = []
let filteredItems: ((ScriptW & { type: 'script' }) | (FlowW & { type: 'flow' }))[] = []
let itemKind: 'script' | 'flow' | 'all' = 'all'
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[] = []
let flows: TableFlow[] = []
let apps: TableApp[] = []
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
let tab: Tab = 'workspace'
let filter: string = ''
@@ -72,15 +94,20 @@
let codeViewerObj: HubItem | undefined = undefined
async function loadScripts(): Promise<void> {
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
}
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
}
@@ -102,14 +129,16 @@
loading = false
}
async function archiveFlow(path: string): Promise<void> {
try {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
loadFlows()
sendUserToast(`Successfully archived flow ${path}`)
} catch (err) {
sendUserToast(`Could not archive this flow ${err.body}`, true)
}
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> {
@@ -122,10 +151,7 @@
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
let combinedItems: (
| (ScriptW & { type: 'script'; time: number })
| (FlowW & { type: 'flow'; time: number })
)[] = []
let combinedItems: (TableScript | TableFlow | TableApp)[] = []
$: combinedItems = [
...flows.map((x) => ({ ...x, type: 'flow' as 'flow', time: new Date(x.edited_at).getTime() })),
@@ -133,7 +159,8 @@
...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 =
@@ -159,6 +186,7 @@
if (($userStore || $superadmin) && $workspaceStore) {
loadScripts()
loadFlows()
loadApps()
loadJobs()
}
}
@@ -178,7 +206,7 @@
interIns
} = info
return idx
const sortResult = idx
.map((v, i) => i)
.sort(
(ia, ib) =>
@@ -202,8 +230,13 @@
(preFilteredItems[idx[ib]].starred ? 100 : 0) -
(preFilteredItems[idx[ia]].starred ? 100 : 0)
)
return sortResult
}
}
$: items = filter !== '' ? filteredItems : preFilteredItems
$: containerHeight = items.length < 5 ? items.length * 80 : 800
</script>
<SearchItems
@@ -232,17 +265,22 @@
<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
<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
variant="border"
>
View on the Hub
</Button>
<Button
href="/scripts/add?hub={encodeURIComponent(codeViewerObj?.path ?? '')}"
startIcon={{ icon: faCodeFork }}>Fork</Button
></div
>
startIcon={{ icon: faCodeFork }}
>
Fork
</Button>
</div>
<HighlightCode language={codeViewerLanguage} code={codeViewerContent} />
</DrawerContent>
@@ -250,15 +288,18 @@
<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
<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
>
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} />
@@ -273,14 +314,15 @@
{:else if $workspaceStore == 'starter'}
<div class="my-4" />
<Alert title="Stater workspace"
>The starter workspace has all its elements (variables, resources, scripts, flows) shared
<Alert title="Stater workspace">
The starter workspace has all its elements (variables, resources, scripts, flows) shared
across all other workspaces. Useful to seed workspace with common elements within your
organization.</Alert
>
organization.
</Alert>
{/if}
<PageHeader title="Home">
<div class="flex flex-row gap-8">
<div class="flex flex-row gap-2">
<CreateApp />
<CreateActionsScript />
<CreateActionsFlow />
</div>
@@ -288,75 +330,141 @@
<div class="my-6" />
<Tabs bind:selected={tab}>
<Tab size="xl" value="workspace"><Icon data={faBuilding} class="mr-2" />Workspace</Tab>
<Tab value="hubscripts"><Icon data={faGlobe} class="mr-2" />Hub Scripts</Tab>
<Tab value="hubflows"><Icon data={faGlobe} class="mr-2" />Hub Flows</Tab>
<Tab size="xs" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size="18px" />
Workspace
</div>
</Tab>
<Tab size="xs" value="hubscripts">
<div class="flex gap-2 items-center my-1">
<Globe2 size="18px" />
Hub Scripts
</div>
</Tab>
<Tab size="xs" value="hubflows">
<div class="flex gap-2 items-center my-1">
<Globe2 size="18px" />
Hub Flows
</div>
</Tab>
</Tabs>
<div class="my-2" />
<div class="flex flex-col gap-y-16">
<div class="max-h-screen h-full flex flex-col">
<div class="flex flex-col">
{#if tab == 'workspace'}
<div class="w-12/12 pb-2 flex flex-row my-1 gap-1">
<input
type="text"
autofocus
placeholder="Search Scripts, Flows & Apps"
bind:value={filter}
class="text-2xl grow"
/>
</div>
<div class="flex justify-between items-center gap-2 ">
<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="max-w-min">
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="all" size="xs">All</ToggleButton>
<ToggleButton light position="center" value="script" size="xs"
><Icon data={faScroll} class="mr-1" />Scripts</ToggleButton
>
<ToggleButton light position="right" value="flow" size="xs"
><Icon data={faWind} class="mr-1" />Flows</ToggleButton
>
</ToggleButtonGroup>
</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}&cross;{/if}
</Badge>
{/each}
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mt-2 w-full overflow-auto">
{#if !loading}
{#each filter != '' ? filteredItems : preFilteredItems as item (item.type + item.path)}
{#if item.type == 'script'}
<ScriptBox
starred={item.starred}
marked={item.marked}
on:change={loadScripts}
script={item}
shareModal={shareModalScripts}
<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"
/>
{:else if item.type == 'flow'}
<FlowBox
starred={item.starred ?? false}
marked={item.marked}
on:change={loadFlows}
flow={item}
shareModal={shareModalFlows}
/>
{/if}
</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}&cross;{/if}
</Badge>
</div>
{/each}
</div>
{/if}
<div>
{#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}
{#each Array(10).fill(0) as sk}
<Skeleton layout={[[4]]} />
{/each}
<div class={classNames('border rounded-md')} style="height:calc(100vh - 18em)">
<VirtualList {items} let:item>
{#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-sm">{pluralize(items.length, 'item')}</span>
{/if}
</div>
{:else if tab == 'hubscripts'}
@@ -365,36 +473,5 @@
<PickHubFlow bind:filter on:pick={(e) => viewFlow(e.detail)} />
{/if}
</div>
<div>
<h2 class="border-b mb-4 py-2">
<span class="text-black-gradient">Runs</span>
</h2>
<div class="grid grid-cols-1 gap-4 my-4">
<Skeleton {loading} layout={[[6], 1, [6], 1, [6]]} />
{#each jobs.splice(0, 3) as job}
<JobDetail {job} />
{/each}
<a
href="/runs"
class="text-sm font-extrabold text-gray-700 hover:underline inline-flex items-center"
>
All runs
<svg
class="w-4 h-4 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</a>
</div>
</div>
</div>
</CenteredPage>
+6 -6
View File
@@ -106,11 +106,11 @@
<h2>Are you in the right workspace?</h2>
<div class="flex flex-col gap-2">
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
<div
><Button variant="border" href="/run/{$page.params.run}?workspace={workspace.id}"
>See in {workspace.name}</Button
></div
>
<div>
<Button variant="border" href="/run/{$page.params.run}?workspace={workspace.id}">
See in {workspace.name}
</Button>
</div>
{/each}
<div>
<Button href="/runs">Go to runs page</Button>
@@ -307,7 +307,7 @@
<Skeleton loading={!job} layout={[[5]]} />
{#if job}
<div class="flex flex-row border rounded-md p-3 max-h-1/2 overflow-auto">
<div class="flex flex-row border rounded-md p-2 mt-2 max-h-1/2 overflow-auto">
{#if viewTab == 'logs'}
<div class="w-full">
<LogViewer isLoading={!(job && 'logs' in job && job.logs)} content={job?.logs} />
+9 -3
View File
@@ -176,15 +176,19 @@
on:click={() => {
goto('/runs?' + $page.url.searchParams.toString())
}}
size="xs"><Icon data={faSearchMinus} /></Button
size="xs"
>
<Icon data={faSearchMinus} />
</Button>
<Button
variant="border"
on:click={() => {
goto('/runs?' + $page.url.searchParams.toString())
}}
size="xs"><Icon data={faSearch} /></Button
size="xs"
>
<Icon data={faSearch} />
</Button>
</div>
<div class="max-w-7x mt-2">
@@ -248,8 +252,10 @@
maxTs = undefined
jobs = await fetchJobs(maxTs, minTs)
}}
size="xs"><Icon data={faSearchMinus} /></Button
size="xs"
>
<Icon data={faSearchMinus} />
</Button>
</div>
<Skeleton loading={!jobs} layout={[[6], 1, [6], 1, [6], 1, [6], 1, [6]]} />
{#if jobs}
@@ -16,8 +16,7 @@
canWrite,
defaultIfEmptyString,
scriptToHubUrl,
copyToClipboard,
emptyString
copyToClipboard
} from '$lib/utils'
import {
faPlay,
@@ -269,9 +268,9 @@
startIcon={{ icon: faArrowLeft }}
href="/scripts/get/{script.parent_hashes[0]}"
>
<svelte:fragment slot="main"
>Previous version ({script.parent_hashes.length})</svelte:fragment
>
<svelte:fragment slot="main">
Previous version ({script.parent_hashes.length})
</svelte:fragment>
{#each script.parent_hashes as hash}
<ButtonPopupItem href="/scripts/get/{hash}" btnClasses="!m-0">
@@ -283,7 +282,7 @@
</div>
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
<div class="flex flex-col gap-2 mb-2">
<div class="flex flex-col gap-2 my-2">
{#if script.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Error deploying this script</p>
@@ -161,12 +161,14 @@
<div class="flex justify-between items-center mt-10">
{#if $superadmin}
<Button variant="border" size="sm" on:click={superadminSettings.openDrawer}>
<Icon data={faCrown} class="mr-1" scale={1} />Superadmin settings</Button
>
<Icon data={faCrown} class="mr-1" scale={1} />
Superadmin settings
</Button>
{/if}
<Button variant="border" size="sm" on:click={userSettings.openDrawer}>
<Icon data={faUserCog} class="mr-1" scale={1} />User settings</Button
>
<Icon data={faUserCog} class="mr-1" scale={1} />
User settings
</Button>
<Button
variant="border"
color="blue"
+2 -4
View File
@@ -24,8 +24,6 @@
faPlus,
faCircle,
faEyeSlash,
faRotateRight,
faArrowRotateRight,
faRefresh,
faChain,
faTrash,
@@ -164,8 +162,8 @@
<Popover>
<Icon data={faRefresh} />
<div slot="text">
This OAuth token will be kept up-to-date in the background by Windmill using
its refresh token
This OAuth token will be kept up-to-date in the background by Windmill
using its refresh token
</div>
</Popover>
{/if}
-6
View File
@@ -83,9 +83,3 @@
</TableCustom>
{/each}
</CenteredPage>
<style>
td {
@apply text-xs;
}
</style>
+1 -2
View File
@@ -178,8 +178,7 @@ const config = {
},
mark: {
backgroundColor: theme('colors.yellow.200'),
border: `1px solid ${theme('colors.gray.600')}`,
borderRadius: theme('borderRadius.sm'),
borderRadius: theme('borderRadius.sm')
},
h3: {
fontSize: '18px',