overhaul app creation flow

This commit is contained in:
Ruben Fiszel
2022-12-31 13:34:59 +01:00
parent f03836494e
commit 0fadc26385
20 changed files with 289 additions and 124 deletions

View File

@@ -1,77 +0,0 @@
<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'
import { LayoutDashboard } from 'lucide-svelte'
let drawerOpen = false
function closeDrawer() {
drawerOpen = false
}
async function createApp() {
const appJson: App = {
grid: [],
title: 'New app',
unusedInlineScripts: [],
fullscreen: false
}
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="Create 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>
<div slot="actions">
<Button on:click={() => createApp()}>Create app</Button>
</div>
</DrawerContent>
</Drawer>
<Button
size="sm"
spacingSize="lg"
startIcon={{ icon: faPlus }}
on:click={() => (drawerOpen = true)}
>
New App (alpha) <LayoutDashboard class="ml-1.5" size={18} />
</Button>

View File

@@ -27,8 +27,7 @@
import { userStore } from '$lib/stores'
import InlineScriptsPanel from './inlineScriptsPanel/InlineScriptsPanel.svelte'
import TablePanel from './TablePanel.svelte'
import { grid } from 'd3-dag'
import SettingsPanel from './SettingsPanel.svelte'
import { fly } from 'svelte/transition'
import type { Policy } from '$lib/gen'
@@ -72,18 +71,7 @@
function saveDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(
() =>
localStorage.setItem(
'app',
encodeState({
path,
value: $appStore,
policy
})
),
500
)
timeout = setTimeout(() => localStorage.setItem('app', encodeState($appStore)), 500)
}
let mounted = false

View File

@@ -1,16 +1,20 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { Drawer, DrawerContent } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
import Path from '$lib/components/Path.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { AppService, Policy } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { faExternalLink } from '@fortawesome/free-solid-svg-icons'
import { faExternalLink, faSave } from '@fortawesome/free-solid-svg-icons'
import { Eye, Laptop2, Pencil, PenTool, Smartphone } from 'lucide-svelte'
import { getContext } from 'svelte'
import { sendUserToast } from '../../../utils'
import type { AppEditorContext, EditorBreakpoint, EditorMode } from '../types'
import AppExportButton from './AppExportButton.svelte'
const { app } = getContext<AppEditorContext>('AppEditorContext')
export let title: string = $app.title || ''
@@ -21,7 +25,42 @@
save: false
}
let newPath: string = ''
let pathError: string | undefined = undefined
let drawerOpen = false
function closeDrawer() {
drawerOpen = false
}
async function createApp(path: string) {
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: $app,
path,
summary: 'App summary',
policy
}
})
goto(`/apps/edit/${appId}`)
} catch (e) {
sendUserToast('Error creating app', e)
}
}
async function save() {
if ($page.params.path == undefined) {
drawerOpen = true
return
}
loading.save = true
AppService.updateApp({
workspace: $workspaceStore!,
@@ -48,6 +87,26 @@
}
</script>
<Drawer bind:open={drawerOpen} size="800px">
<DrawerContent title="Create an App" on:close={() => closeDrawer()}>
<Path
bind:error={pathError}
bind:path={newPath}
initialPath=""
namePlaceholder="my_app"
kind="app"
/>
<div slot="actions">
<Button
startIcon={{ icon: faSave }}
disabled={pathError != ''}
on:click={() => createApp(newPath)}>Create app</Button
>
</div>
</DrawerContent>
</Drawer>
<div class="border-b flex flex-row justify-between py-1 px-4 items-center">
<input class="text-sm w-64" bind:value={title} />
<div class="flex gap-8 items-center">
@@ -73,13 +132,14 @@
</ToggleButtonGroup>
</div>
<Toggle
size="xs"
bind:checked={$app.fullscreen}
options={{ left: 'max-width (1152px)', right: 'fullscreen' }}
/>
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">Centered</ToggleButton>
<ToggleButton position="right" value={true} size="xs">Full Width</ToggleButton>
</ToggleButtonGroup>
</div>
<div class="flex flex-row gap-2 w-64 justify-end">
<div class="flex flex-row gap-4 justify-end">
<AppExportButton app={$app} />
<Button
on:click={() => sendUserToast('Publishing apps publically at secret urls is coming soon')}
color="dark"
@@ -89,6 +149,12 @@
>
Publish
</Button>
<Button loading={loading.save} on:click={save} color="dark" size="xs">Save</Button>
<Button
loading={loading.save}
startIcon={{ icon: faSave }}
on:click={save}
color="dark"
size="xs">Save</Button
>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import { copyToClipboard } from '$lib/utils'
import { faClipboard, faFileExport } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Highlight } from 'svelte-highlight'
import json from 'svelte-highlight/languages/json'
import { Button } from '../../common'
import type { App } from '../types'
let jsonViewerDrawer: Drawer
export let app: App
</script>
<Button
btnClasses="pr-4"
size="sm"
variant="border"
color="light"
on:click={() => jsonViewerDrawer.toggleDrawer()}
>
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
Export JSON
</Button>
<Drawer bind:this={jsonViewerDrawer} size="800px">
<DrawerContent title="App JSON" on:close={() => jsonViewerDrawer.toggleDrawer()}>
<div class="relative">
<Button
on:click={() => copyToClipboard(JSON.stringify(app, null, 4))}
color="dark"
variant="border"
size="sm"
startIcon={{ icon: faClipboard }}
btnClasses="absolute top-2 right-2"
>
Copy content
</Button>
<Highlight language={json} code={JSON.stringify(app, null, 4)} />
</div>
</DrawerContent>
</Drawer>

View File

@@ -12,6 +12,7 @@
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { isOpenStore } from './store'
import { gridColumns } from '../../gridUtils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
@@ -57,6 +58,7 @@
}
function addComponent(appComponent: AppComponent) {
$dirtyStore = true
const grid = $app.grid ?? []
const id = getNextId(grid.map((gridItem) => gridItem.data.id))

View File

@@ -0,0 +1,7 @@
import { writable } from "svelte/store";
import type {
App,
} from './types'
export const importStore = writable<App | undefined>(undefined)

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { faBarsStaggered, faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
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 { Icon } from 'svelte-awesome'
import { importStore } from '../apps/store'
import { LayoutDashboard } from 'lucide-svelte'
let drawer: Drawer | undefined = undefined
let pendingJson: string
async function importJson() {
$importStore = JSON.parse(pendingJson)
await goto('/apps/add')
drawer?.closeDrawer?.()
}
</script>
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup
size="sm"
spacingSize="xl"
startIcon={{ icon: faPlus }}
href="/apps/add?nodraft=true"
>
<svelte:fragment slot="main"
>New App (alpha) <LayoutDashboard class="ml-1.5" size={18} />
</svelte:fragment>
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
Import from raw JSON
</ButtonPopupItem>
</ButtonPopup>
</div>
<!-- Raw JSON -->
<Drawer bind:this={drawer} size="800px">
<DrawerContent title="Import app from JSON" on:close={() => drawer?.toggleDrawer?.()}>
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" fixedOverflowWidgets={false} />
<svelte:fragment slot="actions">
<Button size="sm" on:click={importJson}>Import</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>

View File

@@ -23,7 +23,12 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/flows/add">
<ButtonPopup
size="sm"
spacingSize="xl"
startIcon={{ icon: faPlus }}
href="/flows/add?nodraft=true"
>
<svelte:fragment slot="main"
>New Flow <Icon data={faBarsStaggered} scale={0.8} class="ml-1.5" />
</svelte:fragment>

View File

@@ -27,7 +27,7 @@
</div>
<Drawer bind:this={jsonViewerDrawer} size="800px">
<DrawerContent title="See JSON" on:close={() => jsonViewerDrawer.toggleDrawer()}>
<DrawerContent title="OpenFlow JSON" on:close={() => jsonViewerDrawer.toggleDrawer()}>
{#if $flowStore}
<FlowViewer flow={cleanInputs($flowStore)} tab="json" />
{/if}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { workspaceStore, userWorkspaces } from '$lib/stores'
import { workspaceStore, userWorkspaces, switchWorkspace } from '$lib/stores'
import { classNames } from '$lib/utils'
import Icon from 'svelte-awesome'
import { Building } from 'lucide-svelte'
@@ -33,7 +33,7 @@
<tr
class="text-xs cursor-pointer hover:bg-gray-100"
on:click={() => {
workspaceStore.set(workspace.id)
switchWorkspace(workspace.id)
close()
}}
>

View File

@@ -69,8 +69,6 @@ export const hubScripts = writable<
if (browser) {
workspaceStore.subscribe(async (workspace) => {
if (workspace) {
localStorage.removeItem("flow")
localStorage.removeItem("app")
localStorage.setItem("workspace", String(workspace));
userStore.set(await getUserExt(workspace));
} else {
@@ -79,6 +77,12 @@ if (browser) {
});
}
export function switchWorkspace(workspace: string | undefined) {
localStorage.removeItem("flow")
localStorage.removeItem("app")
workspaceStore.set(workspace);
}
export function clearStores(): void {
localStorage.removeItem("flow")
localStorage.removeItem("app")

View File

@@ -14,8 +14,8 @@
import HighlightCode from '$lib/components/HighlightCode.svelte'
import { Building, Globe2 } from 'lucide-svelte'
import CreateApp from '$lib/components/apps/CreateApp.svelte'
import ItemsList from '$lib/components/home/ItemsList.svelte'
import CreateActionsApp from '$lib/components/flows/CreateActionsApp.svelte'
type Tab = 'hubscripts' | 'hubflows' | 'workspace'
@@ -132,9 +132,9 @@
<PageHeader title="Home">
<div class="flex flex-row gap-3 flex-wrap">
{#if !$userStore?.operator}
<CreateApp />
<CreateActionsScript />
<CreateActionsFlow />
<CreateActionsApp />
{/if}
</div>
</PageHeader>

View File

@@ -0,0 +1,5 @@
export function load() {
return {
stuff: { title: `New App` }
}
}

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import { importStore } from '$lib/components/apps/store'
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
import { Policy } from '$lib/gen'
import { page } from '$app/stores'
import { decodeState } from '$lib/utils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { userStore } from '$lib/stores'
import type { App } from '$lib/components/apps/types'
import { goto } from '$app/navigation'
let nodraft = $page.url.searchParams.get('nodraft')
if (nodraft) {
goto('?', { replaceState: true })
}
const importJson = $importStore
if ($importStore) {
$importStore = undefined
}
const initialState = nodraft ? undefined : localStorage.getItem('app')
let value: App =
importJson ??
(initialState != undefined
? decodeState(initialState)
: {
grid: [],
title: 'New App',
fullscreen: false,
unusedInlineScripts: []
})
$dirtyStore = false
</script>
{#if value}
<div class="h-screen">
<AppEditor
app={value}
path={''}
policy={{
on_behalf_of: `u/${$userStore?.username}`,
execution_mode: Policy.execution_mode.PUBLISHER
}}
/>
</div>
{/if}

View File

@@ -0,0 +1,5 @@
export function load({ params }) {
return {
stuff: { title: `Edit App ${params.path}` }
}
}

View File

@@ -11,8 +11,6 @@
let path = $page.params.path
let nodraft = $page.url.searchParams.get('nodraft')
const initialState = nodraft ? undefined : localStorage.getItem('app')
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
if (nodraft) {
goto('?', { replaceState: true })
@@ -21,15 +19,16 @@
let loading = true
async function loadApp(): Promise<void> {
loading = true
console.log(stateLoadedFromUrl)
app =
stateLoadedFromUrl != undefined && stateLoadedFromUrl?.path == path
? stateLoadedFromUrl
: await AppService.getAppByPath({
path,
workspace: $workspaceStore!
})
app = await AppService.getAppByPath({
path,
workspace: $workspaceStore!
})
const initialState = nodraft ? undefined : localStorage.getItem('app')
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
if (stateLoadedFromUrl) {
app.value = stateLoadedFromUrl
}
loading = false
$dirtyStore = false
}

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
@@ -8,9 +9,15 @@
import { userStore, workspaceStore } from '$lib/stores'
import { decodeState, emptySchema } from '$lib/utils'
let nodraft = $page.url.searchParams.get('nodraft')
if (nodraft) {
goto('?', { replaceState: true })
}
const hubId = $page.url.searchParams.get('hub')
const templatePath = $page.url.searchParams.get('template')
const initialState = hubId || templatePath ? undefined : localStorage.getItem('flow')
const initialState = hubId || templatePath || nodraft ? undefined : localStorage.getItem('flow')
let selectedId: string = 'settings-metadata'
let loading = false

View File

@@ -11,6 +11,8 @@
// Default
let schema: Schema = emptySchema()
let nodraft = $page.url.searchParams.get('nodraft')
const templatePath = $page.url.searchParams.get('template')
const hubPath = $page.url.searchParams.get('hub')

View File

@@ -4,7 +4,7 @@
import { sendUserToast, validateUsername } from '$lib/utils'
import { logoutWithRedirect } from '$lib/logout'
import { page } from '$app/stores'
import { usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { switchWorkspace, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import CenteredModal from '$lib/components/CenteredModal.svelte'
import { Button } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
@@ -56,7 +56,8 @@
sendUserToast(`Created workspace id: ${id}`)
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
workspaceStore.set(id)
switchWorkspace(id)
goto(rd ?? '/')
}
@@ -135,8 +136,8 @@
options={{ right: `Auto invite users as operators` }}
/>
<Tooltip
>An operator can only execute and view scripts/flows/apps from your workspace, and only
those that he has visibility on</Tooltip
>An operator can only execute and view scripts/flows/apps from your workspace, and only those
that he has visibility on</Tooltip
>
</div>
{#if !isDomainAllowed}

View File

@@ -4,7 +4,13 @@
import { sendUserToast } from '$lib/utils'
import { logout, logoutWithRedirect } from '$lib/logout'
import { UserService, type WorkspaceInvite, WorkspaceService } from '$lib/gen'
import { superadmin, usersWorkspaceStore, userWorkspaces, workspaceStore } from '$lib/stores'
import {
superadmin,
switchWorkspace,
usersWorkspaceStore,
userWorkspaces,
workspaceStore
} from '$lib/stores'
import { faCrown, faUserCog } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Button, Skeleton } from '$lib/components/common'
@@ -38,7 +44,7 @@
}
if ($usersWorkspaceStore) {
if (!$workspaceStore) {
workspaceStore.set(localStorage.getItem('workspace')?.toString())
switchWorkspace(localStorage.getItem('workspace')?.toString())
}
} else {
await logoutWithRedirect($page.url.pathname + $page.url.search)