feat: add hub support for apps

This commit is contained in:
Ruben Fiszel
2023-01-07 07:44:44 +01:00
parent b5edb7ed98
commit 50453ca690
24 changed files with 499 additions and 110 deletions
+69
View File
@@ -1769,6 +1769,75 @@ paths:
flow:
$ref: "../../openflow.openapi.yaml#/components/schemas/OpenFlow"
/apps/hub/list:
get:
summary: list all available hub apps
operationId: listHubApps
tags:
- app
responses:
"200":
description: hub apps list
content:
application/json:
schema:
type: object
properties:
apps:
type: array
items:
type: object
properties:
id:
type: number
app_id:
type: number
summary:
type: string
apps:
type: array
items:
type: string
approved:
type: boolean
votes:
type: number
required:
- id
- app_id
- summary
- apps
- approved
- votes
/apps/hub/get/{id}:
get:
summary: get hub app by id
operationId: getHubAppById
tags:
- app
parameters:
- $ref: "#/components/parameters/PathId"
responses:
"200":
description: app
content:
application/json:
schema:
type: object
properties:
app:
type: object
properties:
summary:
type: string
value: {}
required:
- summary
- value
required:
- app
/scripts/hub/get/{path}:
get:
summary: get hub script content by path
+41 -1
View File
@@ -20,6 +20,7 @@ use axum::{
};
use hyper::StatusCode;
use magic_crypt::MagicCryptTrait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use sha2::{Digest, Sha256};
@@ -31,7 +32,9 @@ use windmill_common::{
apps::ListAppQuery,
error::{to_anyhow, Error, JsonResult, Result},
users::owner_to_token_owner,
utils::{not_found_if_none, paginate, Pagination, StripPath},
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, JobPayload, RawCode};
@@ -53,6 +56,12 @@ pub fn unauthed_service() -> Router {
.route("/public_app/:secret", get(get_public_app_by_secret))
}
pub fn global_service() -> Router {
Router::new()
.route("/hub/list", get(list_hub_apps))
.route("/hub/get/:id", get(get_hub_app_by_id))
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct ListableApp {
pub id: i64,
@@ -352,6 +361,37 @@ async fn create_app(
Ok((StatusCode::CREATED, app.path))
}
async fn list_hub_apps(
Authed { email, .. }: Authed,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let flows = list_elems_from_hub(
http_client,
"https://hub.windmill.dev/searchUiData?approved=true",
&email,
)
.await?;
Ok(Json(flows))
}
pub async fn get_hub_app_by_id(
Authed { email, .. }: Authed,
Path(id): Path<i32>,
Extension(http_client): Extension<Client>,
) -> JsonResult<serde_json::Value> {
let value = http_get_from_hub(
http_client,
&format!("https://hub.windmill.dev/apps/{id}/json"),
&email,
false,
)
.await?
.json()
.await
.map_err(to_anyhow)?;
Ok(Json(value))
}
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
+1
View File
@@ -141,6 +141,7 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service())
.nest("/flows", flows::global_service())
.nest("/apps", apps::global_service())
.nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>())
@@ -26,7 +26,6 @@
let runnableComponent: RunnableComponent
let isLoading: boolean = false
let ownClick: boolean = false
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
@@ -40,13 +39,8 @@
$: outputs?.loading.subscribe({
next: (value) => {
isLoading = value
if (ownClick && !value) {
ownClick = false
}
}
})
$: loading = isLoading && ownClick
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
@@ -54,6 +48,7 @@
<InputValue {id} input={configuration.size} bind:value={size} />
<RunnableWrapper
noMinH
bind:runnableComponent
bind:componentInput
{id}
@@ -62,7 +57,7 @@
forceSchemaDisplay={true}
>
<AlignWrapper {horizontalAlignment}>
<div class="flex flex-col gap-2 px-4 w-full">
<div class="flex flex-col gap-2 px-4 w-full ">
<div>
{#if componentInput?.type != 'runnable' || Object.values(componentInput?.fields ?? {}).filter((x) => x.type == 'user').length == 0}
<span class="text-gray-600 italic text-sm py-2">
@@ -73,14 +68,13 @@
</div>
<div class="flex justify-end">
<Button
{loading}
btnClasses="mt-1"
loading={isLoading}
btnClasses="my-1"
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={() => {
ownClick = true
runnableComponent?.runComponent()
if (recomputeIds) {
@@ -4,7 +4,7 @@
import SchemaForm from '$lib/components/SchemaForm.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { AppService, type CompletedJob } from '$lib/gen'
import { defaultIfEmptyString, emptySchema } from '$lib/utils'
import { defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { getContext, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
@@ -21,16 +21,17 @@
export let autoRefresh: boolean = true
export let result: any = undefined
export let forceSchemaDisplay: boolean = false
export let noMinH = false
const { worldStore, runnableComponents, workspace, appPath, isEditor, jobs } =
const { worldStore, runnableComponents, workspace, appPath, isEditor, jobs, noBackend } =
getContext<AppEditorContext>('AppEditorContext')
onMount(() => {
if (autoRefresh) {
$runnableComponents[id] = async () => {
await executeComponent()
await executeComponent(true)
}
executeComponent()
executeComponent(true)
}
})
@@ -43,7 +44,7 @@
function setDebouncedExecute() {
executeTimeout && clearTimeout(executeTimeout)
executeTimeout = setTimeout(() => {
executeComponent()
executeComponent(true)
}, 200)
}
@@ -176,7 +177,13 @@
[]
)
async function executeComponent() {
async function executeComponent(noToast = false) {
if (noBackend) {
if (!noToast) {
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
}
return
}
if (runnable?.type === 'runnableByName' && !runnable.inlineScript) {
return
}
@@ -294,7 +301,7 @@
<slot />
</div>
{:else}
<div class="grow min-w-1/2 min-h-[66%]">
<div class="grow min-w-1/2 {noMinH ? '' : 'min-h-[66%]'}">
<slot />
</div>
{/if}
@@ -1,5 +1,7 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import type { AppInput } from '../../inputType'
import type { AppEditorContext } from '../../types'
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
import NonRunnableComponent from './NonRunnableComponent.svelte'
import RunnableComponent from './RunnableComponent.svelte'
@@ -7,12 +9,22 @@
export let componentInput: AppInput | undefined
export let id: string
export let result: any = undefined
export let noMinH = false
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
export let runnableComponent: RunnableComponent | undefined = undefined
export let forceSchemaDisplay: boolean = false
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
if (noBackend && componentInput?.type == 'runnable') {
result = componentInput?.['value']
}
onMount(() => {
$staticExporter[id] = () => result
})
function isRunnableDefined() {
return isScriptByNameDefined(componentInput) || isScriptByPathDefined(componentInput)
}
@@ -30,6 +42,7 @@
{id}
{extraQueryParams}
{forceSchemaDisplay}
{noMinH}
>
<slot />
</RunnableComponent>
@@ -69,7 +69,9 @@
workspace: $workspaceStore ?? '',
onchange: () => saveDraft(),
isEditor: true,
jobs: writable([])
jobs: writable([]),
staticExporter: writable({}),
noBackend: false
})
let timeout: NodeJS.Timeout | undefined = undefined
@@ -128,6 +130,7 @@
{policy}
isEditor
{context}
noBackend={false}
/>
{:else}
<SplitPanesWrapper>
@@ -8,7 +8,7 @@
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import FlowJobResult from '$lib/components/FlowJobResult.svelte'
import Dropdown from '$lib/components/Dropdown.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
@@ -19,21 +19,29 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { AppService, Job, Policy } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { faBug, faClipboard, faExternalLink, faSave } from '@fortawesome/free-solid-svg-icons'
import {
faBug,
faClipboard,
faExternalLink,
faFileExport,
faGlobe,
faSave
} from '@fortawesome/free-solid-svg-icons'
import {
AlignHorizontalSpaceAround,
Expand,
Eye,
Laptop2,
Loader2,
MoreVertical,
Pencil,
Smartphone
} from 'lucide-svelte'
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { classNames, copyToClipboard, sendUserToast } from '../../../utils'
import type { AppComponent, AppEditorContext } from '../types'
import { appToHubUrl, classNames, copyToClipboard, sendUserToast } from '../../../utils'
import type { App, AppComponent, AppEditorContext } from '../types'
import AppExportButton from './AppExportButton.svelte'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
@@ -47,7 +55,7 @@
export let policy: Policy
const { app, summary, mode, breakpoint, appPath, jobs } =
const { app, summary, mode, breakpoint, appPath, jobs, staticExporter } =
getContext<AppEditorContext>('AppEditorContext')
const loading = {
publish: false,
@@ -57,6 +65,8 @@
let newPath: string = ''
let pathError: string | undefined = undefined
let appExport: AppExportButton
let saveDrawerOpen = false
let jobsDrawerOpen = false
let publishDrawerOpen = false
@@ -65,6 +75,17 @@
saveDrawerOpen = false
}
function toStatic(): { app: App; summary: string } {
const newApp: App = JSON.parse(JSON.stringify($app))
newApp.grid.forEach((x) => {
let c: AppComponent = x.data
if (c.componentInput?.type == 'runnable') {
c.componentInput.value = $staticExporter[x.id]()
}
})
return { app: newApp, summary: $summary }
}
async function computeTriggerables() {
const allTriggers = await Promise.all(
$app.grid.map(async (x) => {
@@ -333,7 +354,7 @@
</Drawer>
<div
class="border-b flex flex-row justify-between py-1 gap-4 overflow-x-auto gap-y-2 px-4 items-center"
class="border-b flex flex-row justify-between py-1 gap-4 gap-y-2 px-4 items-center overflow-y-visible"
>
<div class="min-w-64 w-64">
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
@@ -344,11 +365,13 @@
<ToggleButton position="left" value="dnd" size="xs">
<div class="inline-flex gap-1 items-center">
<Pencil size={14} />
Editor
<span class="hidden md:inline">Editor</span>
</div>
</ToggleButton>
<ToggleButton position="right" value="preview" size="xs">
<div class="inline-flex gap-1 items-center"> <Eye size={14} /> Preview</div>
<div class="inline-flex gap-1 items-center">
<Eye size={14} /> <span class="hidden md:inline">Preview</span>
</div>
</ToggleButton>
</ToggleButtonGroup>
</div>
@@ -363,7 +386,7 @@
</ToggleButtonGroup>
</div>
<span class="hidden lg:block">
<div class="hidden lg:block">
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">
<div class="flex gap-1 justify-start">
@@ -378,22 +401,44 @@
<Expand size={14} />
</ToggleButton>
</ToggleButtonGroup>
</span>
</div>
</div>
<div class="flex flex-row grow gap-2 justify-end items-center">
<Button
on:click={() => (jobsDrawerOpen = true)}
color="light"
size="xs"
variant="border"
startIcon={{ icon: faBug }}
<div class="flex flex-row grow gap-2 justify-end items-center overflow-visible">
<Dropdown
placement="bottom-end"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px] hidden lg:block"
dropdownItems={[
{
displayName: 'JSON',
icon: faFileExport,
action: () => {
appExport.open()
}
},
{
displayName: 'Publish to Hub',
icon: faGlobe,
action: () => {
const url = appToHubUrl(toStatic())
window.open(url.toString(), '_blank')
}
}
]}
>
Debug Runs
</Button>
<span class="hidden lg:block">
<AppExportButton app={$app} />
<MoreVertical size={20} />
</Dropdown>
<span class="hidden md:inline">
<Button
on:click={() => (jobsDrawerOpen = true)}
color="light"
size="xs"
variant="border"
startIcon={{ icon: faBug }}
>
<span class="hidden md:inline">Debug Runs</span>
</Button>
</span>
<AppExportButton bind:this={appExport} app={$app} />
<Button
on:click={() => (publishDrawerOpen = true)}
color="light"
@@ -401,7 +446,7 @@
variant="border"
startIcon={{ icon: faExternalLink }}
>
Publish
<span class="hidden md:inline">Publish</span>
</Button>
<Button
loading={loading.save}
@@ -410,7 +455,7 @@
color="dark"
size="xs"
>
Save
<span class="hidden md:inline">Save</span>
</Button>
</div>
</div>
@@ -4,8 +4,7 @@
import { copyToClipboard } from '$lib/utils'
import { faClipboard, faFileExport } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import { Highlight } from 'svelte-highlight'
import json from 'svelte-highlight/languages/json'
import { Button } from '../../common'
@@ -13,14 +12,13 @@
let jsonViewerDrawer: Drawer
export function open() {
jsonViewerDrawer?.toggleDrawer()
}
export let app: App
</script>
<Button size="xs" variant="border" color="light" on:click={() => jsonViewerDrawer.toggleDrawer()}>
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
JSON
</Button>
<Drawer bind:this={jsonViewerDrawer} size="800px">
<DrawerContent title="App JSON" on:close={() => jsonViewerDrawer.toggleDrawer()}>
<div class="relative">
@@ -23,6 +23,7 @@
export let workspace: string
export let isEditor: boolean
export let context: Record<string, any>
export let noBackend: boolean = false
const appStore = writable<App>(app)
const worldStore = writable<World | undefined>(undefined)
@@ -53,7 +54,9 @@
workspace,
onchange: undefined,
isEditor,
jobs: writable([])
jobs: writable([]),
staticExporter: writable({}),
noBackend
})
let mounted = false
@@ -71,6 +71,7 @@ export type ResultInput = {
runnable: Runnable
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
type: 'runnable'
value?: any
}
type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
+3 -1
View File
@@ -142,11 +142,13 @@ export type AppEditorContext = {
connectingInput: Writable<ConnectingInput>
breakpoint: Writable<EditorBreakpoint>
runnableComponents: Writable<Record<string, () => Promise<void>>>
staticExporter: Writable<Record<string, () => any>>
appPath: string,
workspace: string,
onchange: (() => void) | undefined,
isEditor: boolean,
jobs: Writable<{ job: string, component: string }[]>
jobs: Writable<{ job: string, component: string }[]>,
noBackend: boolean
}
export type EditorMode = 'dnd' | 'preview'
@@ -6,6 +6,7 @@
import { AppService, type ListableApp } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import {
faCodeFork,
faEdit,
faEye,
faFileExport,
@@ -57,6 +58,18 @@
Edit
</Button>
</div>
{:else}
<div>
<Button
color="light"
size="xs"
variant="border"
startIcon={{ icon: faCodeFork }}
href="/apps/add?template={path}"
>
Fork
</Button>
</div>
{/if}
{/if}
@@ -1,7 +1,7 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
import { Code2, LayoutDashboard, Wind } from 'lucide-svelte'
import { Code2, LayoutDashboard } from 'lucide-svelte'
import Icon from 'svelte-awesome'
export let kind: 'script' | 'flow' | 'app'
@@ -0,0 +1,81 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { Badge, Skeleton } from '$lib/components/common'
import SearchItems from '$lib/components/SearchItems.svelte'
import { loadHubApps } from '$lib/utils'
import ListFilters from '$lib/components/home/ListFilters.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
export let filter = ''
type Item = { apps: string[]; summary: string; path: string }
let hubApps: any[] | undefined = undefined
let filteredItems: (Item & { marked?: string })[] = []
let appFilter: string | undefined = undefined
$: prefilteredItems = appFilter
? (hubApps ?? []).filter((i) => i.apps.includes(appFilter))
: hubApps ?? []
$: apps = Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort()
const dispatch = createEventDispatcher()
onMount(async () => {
hubApps = await loadHubApps()
})
</script>
<SearchItems
{filter}
items={prefilteredItems}
bind:filteredItems
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
/>
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input type="text" placeholder="Search Hub Apps" bind:value={filter} class="text-2xl grow" />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
{#if hubApps}
{#if filteredItems.length == 0}
<NoItemFound />
{:else}
<ul class="divide-y divide-gray-200 border rounded-md">
{#each filteredItems as item (item)}
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<RowIcon kind="app" />
<div class="w-full text-left font-normal ">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
</div>
</div>
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
{#each item.apps as app}
<Badge color="gray" baseClass="border">{app}</Badge>
{/each}
</div>
</button>
</li>
{/each}
</ul>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{/if}
@@ -35,7 +35,7 @@
/>
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input type="text" placeholder="Search Hub Scripts" bind:value={filter} class="text-2xl grow" />
<input type="text" placeholder="Search Hub Flows" bind:value={filter} class="text-2xl grow" />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
@@ -74,6 +74,8 @@
</ul>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
@@ -88,6 +88,7 @@
</div>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[0.5, [4]]} />
{/each}
+19
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { goto } from '$app/navigation'
import {
AppService,
FlowService,
FolderService,
Script,
@@ -538,6 +539,17 @@ export async function loadHubFlows() {
}
}
export async function loadHubApps() {
try {
const apps = (await AppService.listHubApps()).apps ?? []
const processed = apps.sort((a, b) => b.votes - a.votes)
return processed
} catch {
console.error('Hub is not available')
}
}
export function formatCron(inp: string): string {
// Allow for cron expressions inputted by the user to omit month and year
let splitted = inp.split(' ')
@@ -561,6 +573,13 @@ export function flowToHubUrl(flow: Flow): URL {
return url
}
export function appToHubUrl(staticApp: any): URL {
const url = new URL('https://hub.windmill.dev/apps/add')
url.searchParams.append('app', encodeState(staticApp))
return url
}
export function scriptToHubUrl(
content: string,
summary: string,
@@ -1,5 +1,5 @@
<script lang="ts">
import { FlowService, type OpenFlow } from '$lib/gen'
import { AppService, FlowService, type OpenFlow } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import PageHeader from '$lib/components/PageHeader.svelte'
@@ -16,8 +16,12 @@
import ItemsList from '$lib/components/home/ItemsList.svelte'
import CreateActionsApp from '$lib/components/flows/CreateActionsApp.svelte'
import PickHubApp from '$lib/components/flows/pickers/PickHubApp.svelte'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { writable } from 'svelte/store'
import type { EditorBreakpoint } from '$lib/components/apps/types'
type Tab = 'hubscripts' | 'hubflows' | 'workspace'
type Tab = 'hubscripts' | 'hubflows' | 'hubapps' | 'workspace'
let tab: Tab = 'workspace'
let filter: string = ''
@@ -25,11 +29,16 @@
let flowViewer: Drawer
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
let appViewer: Drawer
let appViewerApp: { app?: any & { id?: number } } | undefined
let codeViewer: Drawer
let codeViewerContent: string = ''
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
let codeViewerObj: HubItem | undefined = undefined
const breakpoint = writable<EditorBreakpoint>('lg')
async function viewCode(obj: HubItem) {
const { content, language } = await getScriptByPath(obj.path)
codeViewerContent = content
@@ -44,6 +53,13 @@
flowViewerFlow = hub
flowViewer.openDrawer?.()
}
async function viewApp(obj: { app_id: number }): Promise<void> {
const hub = await AppService.getHubAppById({ id: obj.app_id })
delete hub['comments']
appViewerApp = hub
appViewer.openDrawer?.()
}
</script>
<Drawer bind:this={codeViewer} size="900px">
@@ -108,6 +124,52 @@
</DrawerContent>
</Drawer>
<Drawer bind:this={appViewer} size="1200px">
<DrawerContent title="Hub app" on:close={appViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
href="https://hub.windmill.dev/apps/{appViewerApp?.app?.id}"
variant="contained"
color="light"
size="xs"
>
<div class="flex gap-2 items-center">
<Globe2 size={18} />
View on the Hub
</div>
</Button>
<Button
href="/apps/add?hub={appViewerApp?.app?.id}"
startIcon={{ icon: faCodeFork }}
color="dark"
size="xs"
>
Fork
</Button>
</svelte:fragment>
{#if appViewerApp?.app}
<div class="p-4">
<AppPreview
app={appViewerApp?.app?.value}
appPath="''"
{breakpoint}
policy={{}}
workspace="hub"
isEditor={false}
context={{
username: $userStore?.username ?? 'anonymous',
email: $userStore?.email ?? 'anonymous'
}}
summary={appViewerApp?.app.summary ?? ''}
noBackend
/>
</div>
{/if}
</DrawerContent>
</Drawer>
<div>
<div class="max-w-6xl mx-auto px-4 sm:px-6 md:px-8 h-fit-content">
{#if $workspaceStore == 'demo'}
@@ -140,26 +202,34 @@
</PageHeader>
{#if !$userStore?.operator}
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="hubscripts">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Scripts
</div>
</Tab>
<Tab size="md" value="hubflows">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Flows
</div>
</Tab>
</Tabs>
<div class="w-full overflow-auto scrollbar-hidden">
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="hubscripts">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Scripts
</div>
</Tab>
<Tab size="md" value="hubflows">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Flows
</div>
</Tab>
<Tab size="md" value="hubapps">
<div class="flex gap-2 items-center my-1">
<Globe2 size={18} />
Hub Apps
</div>
</Tab>
</Tabs>
</div>
{/if}
<div class="my-2" />
<div class="flex flex-col gap-y-16">
@@ -168,6 +238,8 @@
<PickHubScript bind:filter on:pick={(e) => viewCode(e.detail)} />
{:else if tab == 'hubflows'}
<PickHubFlow bind:filter on:pick={(e) => viewFlow(e.detail)} />
{:else if tab == 'hubapps'}
<PickHubApp bind:filter on:pick={(e) => viewApp(e.detail)} />
{/if}
</div>
</div>
@@ -2,55 +2,78 @@
import { importStore } from '$lib/components/apps/store'
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
import { Policy } from '$lib/gen'
import { AppService, Policy } from '$lib/gen'
import { page } from '$app/stores'
import { decodeState, sendUserToast } from '$lib/utils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { userStore } from '$lib/stores'
import { userStore, workspaceStore } 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 hubId = $page.url.searchParams.get('hub')
const templatePath = $page.url.searchParams.get('template')
const importJson = $importStore
if ($importStore) {
$importStore = undefined
}
const initialState = nodraft ? undefined : localStorage.getItem('app')
const state = nodraft ? undefined : localStorage.getItem('app')
let value: App =
importJson ??
(initialState != undefined
? decodeState(initialState)
: {
grid: [],
title: 'New App',
fullscreen: false,
unusedInlineScripts: []
})
if (!importJson && initialState) {
sendUserToast('App restored from draft')
let summary = ''
let value: App = {
grid: [],
fullscreen: false,
unusedInlineScripts: []
}
if (nodraft) {
goto('?', { replaceState: true })
}
loadApp()
async function loadApp() {
if (importJson) {
sendUserToast('Loaded from raw JSON')
value = importJson
} else if (templatePath) {
const template = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: templatePath
})
value = template.value
sendUserToast('App loaded from template')
goto('?', { replaceState: true })
} else if (hubId) {
const hub = await AppService.getHubAppById({ id: Number(hubId) })
value = hub.app.value
summary = hub.app.summary
sendUserToast('App loaded from Hub')
goto('?', { replaceState: true })
} else if (!templatePath && !hubId && state) {
sendUserToast('App restored from draft')
value = decodeState(state)
}
}
$dirtyStore = false
</script>
{#if value}
<div class="h-screen">
<AppEditor
summary={''}
app={value}
path={''}
policy={{
on_behalf_of: `u/${$userStore?.username}`,
on_behalf_of_email: $userStore?.email,
execution_mode: Policy.execution_mode.PUBLISHER
}}
/>
{#key value}
<AppEditor
{summary}
app={value}
path={''}
policy={{
on_behalf_of: `u/${$userStore?.username}`,
on_behalf_of_email: $userStore?.email,
execution_mode: Policy.execution_mode.PUBLISHER
}}
/>
{/key}
</div>
{/if}
@@ -32,6 +32,7 @@
{breakpoint}
policy={app.policy}
isEditor={false}
noBackend={false}
/>
</div>
{:else}
@@ -50,7 +50,7 @@
const oldPath = flow.path.split('/')
flow.path = `u/${$userStore?.username}/${oldPath[oldPath.length - 1]}`
flow = flow
$page.url.searchParams.delete('template')
goto('?', { replaceState: true })
selectedId = 'settings-graph'
} else if (hubId) {
const hub = await FlowService.getHubFlowById({ id: Number(hubId) })
@@ -58,7 +58,7 @@
flow.path = `u/${$userStore?.username}/flow_${hubId}`
Object.assign(flow, hub.flow)
flow = flow
$page.url.searchParams.delete('hub')
goto('?', { replaceState: true })
selectedId = 'settings-graph'
}
}
+2 -2
View File
@@ -11,10 +11,10 @@
// Default toast options
const toastOptions = {
duration: 4000, // duration of progress bar tween to the `next` value
duration: 10000, // duration of progress bar tween to the `next` value
initial: 1, // initial progress bar value
next: 0, // next progress value
pausable: false, // pause progress bar tween on mouse hover
pausable: true, // pause progress bar tween on mouse hover
dismissable: true, // allow dismiss with close button
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
@@ -68,6 +68,7 @@
{:else if app}
<div class="border rounded-md p-2 w-full">
<AppPreview
noBackend={false}
context={{ email: $userStore?.email, username: $userStore?.username }}
workspace={$page.params.workspace}
summary={app.summary}