feat: add support for custom components in react or vanilla JS (#2603)
* cc * cc * cc * all * sqlx
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
14
backend/.sqlx/query-98c8e2c58b38beedcb474251c2554a2f1b768ec7ed1a419313f4842f26425e5d.json
generated
Normal file
14
backend/.sqlx/query-98c8e2c58b38beedcb474251c2554a2f1b768ec7ed1a419313f4842f26425e5d.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}') ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "98c8e2c58b38beedcb474251c2554a2f1b768ec7ed1a419313f4842f26425e5d"
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"Nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
|
||||
23
backend/.sqlx/query-e92536776a1960fa739bbb01d5a8d1b451ff2689a95442bdf820c65f0a8412a5.json
generated
Normal file
23
backend/.sqlx/query-e92536776a1960fa739bbb01d5a8d1b451ff2689a95442bdf820c65f0a8412a5.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value->>'js' FROM resource\n WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "e92536776a1960fa739bbb01d5a8d1b451ff2689a95442bdf820c65f0a8412a5"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) SELECT id, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{"g/all": false}' FROM workspace ON CONFLICT DO NOTHING;
|
||||
@@ -245,6 +245,10 @@ pub async fn run_server(
|
||||
"/w/:workspace_id/jobs_u",
|
||||
jobs::global_service().layer(cors.clone()),
|
||||
)
|
||||
.nest(
|
||||
"/w/:workspace_id/resources_u",
|
||||
resources::public_service().layer(cors.clone()),
|
||||
)
|
||||
.nest(
|
||||
"/w/:workspace_id/capture_u",
|
||||
capture::global_service().layer(cors),
|
||||
|
||||
@@ -12,11 +12,14 @@ use crate::{
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
use axum::{
|
||||
body,
|
||||
extract::{Extension, Path, Query},
|
||||
response::Response,
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use bytes::Bytes;
|
||||
use hyper::{header, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
@@ -56,6 +59,10 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/type/create", post(create_resource_type))
|
||||
}
|
||||
|
||||
pub fn public_service() -> Router {
|
||||
Router::new().route("/custom_component/:name", get(custom_component))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct ResourceType {
|
||||
pub workspace_id: String,
|
||||
@@ -306,6 +313,33 @@ async fn get_resource_value(
|
||||
Ok(Json(value))
|
||||
}
|
||||
|
||||
async fn custom_component(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
) -> Result<Response> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let cc_o = sqlx::query_scalar!(
|
||||
"SELECT value->>'js' FROM resource
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
format!("f/app_custom/{name}"),
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let cc = not_found_if_none(cc_o, "Custom Component", name)?;
|
||||
let res = Response::builder().header(header::CONTENT_TYPE, "text/javascript");
|
||||
|
||||
Ok(res
|
||||
.body(body::boxed(body::Full::from(Bytes::from(cc))))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JobInfo {
|
||||
job_id: Option<Uuid>,
|
||||
|
||||
@@ -1028,6 +1028,13 @@ async fn create_workspace(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}') ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}') ON CONFLICT DO NOTHING",
|
||||
nw.id,
|
||||
|
||||
@@ -133,7 +133,14 @@
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
)
|
||||
.filter((x) => x != initialFolder)
|
||||
.filter(
|
||||
(x) =>
|
||||
x != initialFolder &&
|
||||
x != 'app_groups' &&
|
||||
x != 'app_custom' &&
|
||||
x != 'app_themes' &&
|
||||
x != 'app_custom'
|
||||
)
|
||||
.map((x) => ({
|
||||
name: x,
|
||||
write:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
|
||||
type CCProps<Input> = {
|
||||
id: string
|
||||
render: boolean
|
||||
passSetters: (setter: Setter<Input>) => void
|
||||
setOutput: (output: any) => void
|
||||
}
|
||||
|
||||
interface Setter<Input> {
|
||||
onRender(bool: boolean): void
|
||||
onInput(input: Input): void
|
||||
}
|
||||
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import { RunnableWrapper } from '../helpers'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import type { CustomComponentConfig } from '../../editor/component'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
|
||||
export let id: string
|
||||
export let render: boolean
|
||||
export let componentInput: AppInput | undefined
|
||||
export let customComponent: CustomComponentConfig
|
||||
|
||||
let divId = `custom-component-${id}`
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
output: undefined,
|
||||
loading: false
|
||||
})
|
||||
|
||||
let setInput
|
||||
let setRender
|
||||
let ccProps: CCProps<any> = {
|
||||
render,
|
||||
id: divId,
|
||||
passSetters: (setter) => {
|
||||
setInput = setter.onInput
|
||||
setRender = setter.onRender
|
||||
},
|
||||
setOutput: (output) => {
|
||||
outputs.output.set(output)
|
||||
}
|
||||
}
|
||||
let loaded = false
|
||||
|
||||
// $: renderer && divEl && renderer(ccProps)
|
||||
onMount(async () => {
|
||||
// //@ts-ignore
|
||||
// await import('http://localhost:3000/app.iife.js')
|
||||
/* @vite-ignore */
|
||||
|
||||
if (customComponent?.additionalLibs?.reactVersion) {
|
||||
let reactVersion = customComponent.additionalLibs.reactVersion
|
||||
//@ts-ignore
|
||||
await import(`https://unpkg.com/react@${reactVersion}/umd/react.development.js`)
|
||||
|
||||
//@ts-ignore
|
||||
await import(`https://unpkg.com/react-dom@${reactVersion}/umd/react-dom.development.js`)
|
||||
}
|
||||
//@ts-ignore
|
||||
await import(
|
||||
`http://localhost:3000/api/w/${$workspaceStore ?? 'NO_W'}/resources_u/custom_component/${
|
||||
customComponent.name
|
||||
}`
|
||||
)
|
||||
loaded = true
|
||||
try {
|
||||
let renderer: (props: CCProps<number>) => void =
|
||||
globalThis.windmill[customComponent?.name ?? 'no_name']
|
||||
if (!renderer) {
|
||||
sendUserToast('Custom Component seem to be ill-defined (renderer missing)', true)
|
||||
return
|
||||
}
|
||||
renderer?.(ccProps)
|
||||
} catch (e) {
|
||||
sendUserToast('Custom Component seem to be ill-defined', true)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
let result
|
||||
|
||||
$: render != undefined && handleRender()
|
||||
function handleRender() {
|
||||
setRender?.(render)
|
||||
}
|
||||
$: result != undefined && handleResult()
|
||||
function handleResult() {
|
||||
setInput?.(result)
|
||||
}
|
||||
// $: result && setInput && setInput(result)
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
{#if render}
|
||||
<div class="w-full h-full overflow-auto {customComponent?.name ?? 'no_name'}">
|
||||
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:result>
|
||||
{#if !loaded}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
<div id={divId} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -388,16 +388,20 @@
|
||||
|
||||
let css: string | undefined = undefined
|
||||
|
||||
let lastTheme: string | undefined = undefined
|
||||
appStore.subscribe(async (currentAppStore) => {
|
||||
if (!currentAppStore.theme) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentAppStore.theme.type === 'inlined') {
|
||||
css = currentAppStore.theme.css
|
||||
} else if (currentAppStore.theme.type === 'path' && currentAppStore.theme?.path) {
|
||||
let loadedCss = await getTheme($workspaceStore!, currentAppStore.theme.path)
|
||||
css = loadedCss.value
|
||||
if (JSON.stringify(currentAppStore.theme) != lastTheme) {
|
||||
if (currentAppStore.theme.type === 'inlined') {
|
||||
css = currentAppStore.theme.css
|
||||
} else if (currentAppStore.theme.type === 'path' && currentAppStore.theme?.path) {
|
||||
let loadedCss = await getTheme($workspaceStore!, currentAppStore.theme.path)
|
||||
css = loadedCss.value
|
||||
}
|
||||
lastTheme = JSON.stringify(currentAppStore.theme)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -268,7 +268,8 @@ export function cleanseOneOfConfiguration(
|
||||
|
||||
export function appComponentFromType<T extends keyof typeof components>(
|
||||
type: T,
|
||||
overrideConfiguration?: Partial<InitialAppComponent['configuration']>
|
||||
overrideConfiguration?: Partial<InitialAppComponent['configuration']>,
|
||||
extra?: any
|
||||
): (id: string) => BaseAppComponent & BaseComponent<T> {
|
||||
return (id: string) => {
|
||||
const init = JSON.parse(JSON.stringify(ccomponents[type].initialData)) as InitialAppComponent
|
||||
@@ -304,7 +305,8 @@ export function appComponentFromType<T extends keyof typeof components>(
|
||||
numberOfSubgrids: init.numberOfSubgrids,
|
||||
horizontalAlignment: init.horizontalAlignment,
|
||||
verticalAlignment: init.verticalAlignment,
|
||||
id
|
||||
id,
|
||||
...(extra ?? {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,9 +426,7 @@ export function getAllSubgridsAndComponentIds(
|
||||
return [subgrids, components]
|
||||
}
|
||||
|
||||
export function getAllGridItems(
|
||||
app: App
|
||||
): GridItem[] {
|
||||
export function getAllGridItems(app: App): GridItem[] {
|
||||
return app.grid.concat(Object.values(app.subgrids ?? {}).flat())
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
|
||||
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
|
||||
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
|
||||
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -231,6 +232,13 @@
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'customcomponent'}
|
||||
<AppCustomComponent
|
||||
customComponent={component.customComponent}
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'mardowncomponent'}
|
||||
<AppMarkdown
|
||||
id={component.id}
|
||||
|
||||
@@ -63,6 +63,12 @@ export type RecomputeOthersSource = {
|
||||
recomputeIds: string[] | undefined
|
||||
}
|
||||
|
||||
export type CustomComponentConfig = {
|
||||
name: string
|
||||
additionalLibs?: {
|
||||
reactVersion?: string
|
||||
}
|
||||
}
|
||||
export type TextComponent = BaseComponent<'textcomponent'>
|
||||
export type TextInputComponent = BaseComponent<'textinputcomponent'>
|
||||
export type QuillComponent = BaseComponent<'quillcomponent'>
|
||||
@@ -75,6 +81,9 @@ export type CurrencyComponent = BaseComponent<'currencycomponent'>
|
||||
export type SliderComponent = BaseComponent<'slidercomponent'>
|
||||
export type RangeComponent = BaseComponent<'rangecomponent'>
|
||||
export type HtmlComponent = BaseComponent<'htmlcomponent'>
|
||||
export type CustomComponent = BaseComponent<'customcomponent'> & {
|
||||
customComponent: CustomComponentConfig
|
||||
}
|
||||
export type MarkdownComponent = BaseComponent<'mardowncomponent'>
|
||||
export type VegaLiteComponent = BaseComponent<'vegalitecomponent'>
|
||||
export type PlotlyComponent = BaseComponent<'plotlycomponent'>
|
||||
@@ -173,6 +182,7 @@ export type TypedComponent =
|
||||
| BarChartComponent
|
||||
| TimeseriesComponent
|
||||
| HtmlComponent
|
||||
| CustomComponent
|
||||
| MarkdownComponent
|
||||
| TableComponent
|
||||
| TextComponent
|
||||
@@ -1198,6 +1208,23 @@ Hello \${ctx.username}
|
||||
configuration: {}
|
||||
}
|
||||
},
|
||||
customcomponent: {
|
||||
name: 'Custom',
|
||||
icon: Code2,
|
||||
documentationLink: `${documentationBaseUrl}#custom`,
|
||||
dims: '1:2-1:2' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { class: '', style: '' }
|
||||
},
|
||||
initialData: {
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'object',
|
||||
value: {}
|
||||
},
|
||||
configuration: {}
|
||||
}
|
||||
},
|
||||
mardowncomponent: {
|
||||
name: 'Markdown',
|
||||
icon: Heading1,
|
||||
|
||||
@@ -63,7 +63,8 @@ const display: ComponentSet = {
|
||||
'pdfcomponent',
|
||||
'displaycomponent',
|
||||
'jobidlogcomponent',
|
||||
'jobidflowstatuscomponent'
|
||||
'jobidflowstatuscomponent',
|
||||
'customcomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import { getContext, onMount, tick } from 'svelte'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import {
|
||||
components as componentsRecord,
|
||||
presets as presetsRecord,
|
||||
@@ -11,10 +11,13 @@
|
||||
import ListItem from './ListItem.svelte'
|
||||
import { appComponentFromType, copyComponent, insertNewGridItem } from '../appUtils'
|
||||
import { push } from '$lib/history'
|
||||
import { ClearableInput } from '../../../common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ClearableInput, Drawer, DrawerContent } from '../../../common'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { getGroup, listGroups } from './groupUtils'
|
||||
import { LayoutDashboard } from 'lucide-svelte'
|
||||
import { LayoutDashboard, Plus } from 'lucide-svelte'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import ComponentsList from './CustomComponentsList.svelte'
|
||||
|
||||
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -25,10 +28,20 @@
|
||||
path: string
|
||||
}> = []
|
||||
|
||||
let customComponents: Array<{
|
||||
name: string
|
||||
path: string
|
||||
}> = []
|
||||
|
||||
async function fetchGroups() {
|
||||
if ($workspaceStore) {
|
||||
groups = await listGroups($workspaceStore)
|
||||
}
|
||||
groups = await listGroups($workspaceStore ?? '')
|
||||
}
|
||||
|
||||
async function fetchCustomComponents() {
|
||||
customComponents = await ResourceService.listResourceNames({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name: 'app_custom'
|
||||
})
|
||||
}
|
||||
|
||||
function addComponent(appComponentType: TypedComponent['type']): string {
|
||||
@@ -61,6 +74,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewGroup() {
|
||||
push(history, $app)
|
||||
|
||||
const id = insertNewGridItem(
|
||||
$app,
|
||||
appComponentFromType('containercomponent', undefined, { groupFields: {} }) as (
|
||||
id: string
|
||||
) => AppComponent,
|
||||
$focusedGrid
|
||||
)
|
||||
|
||||
if (id) {
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
}
|
||||
}
|
||||
|
||||
async function addCustomComponent(cc: { name: string; path: string }) {
|
||||
if (!$workspaceStore) return
|
||||
let res: any = undefined
|
||||
try {
|
||||
res = await ResourceService.getResourceValue({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: cc.path
|
||||
})
|
||||
} catch (e) {
|
||||
sendUserToast(`Custom Component not found at ${cc.path}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!res) return
|
||||
|
||||
push(history, $app)
|
||||
|
||||
const id = insertNewGridItem(
|
||||
$app,
|
||||
appComponentFromType('customcomponent', undefined, {
|
||||
customComponent: {
|
||||
name: cc.name.replace(/-/g, '_').replace(/\s/g, '_'),
|
||||
additionalLibs: {
|
||||
reactVersion: '18.2.0'
|
||||
}
|
||||
}
|
||||
}) as (id: string) => AppComponent,
|
||||
$focusedGrid
|
||||
)
|
||||
|
||||
if (id) {
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
}
|
||||
}
|
||||
|
||||
function addPresetComponent(appComponentType: string): void {
|
||||
const preset = presetsRecord[appComponentType]
|
||||
|
||||
@@ -92,14 +158,25 @@
|
||||
})
|
||||
}))
|
||||
|
||||
onMount(() => {
|
||||
fetchGroups()
|
||||
})
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
fetchGroups()
|
||||
fetchCustomComponents()
|
||||
}
|
||||
}
|
||||
|
||||
let dndTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let ccDrawer: Drawer
|
||||
</script>
|
||||
|
||||
<section class="p-2 sticky w-full z-10 top-0 bg-surface border-b">
|
||||
<Drawer bind:this={ccDrawer}>
|
||||
<DrawerContent title="Custom Components" on:close={ccDrawer.closeDrawer}>
|
||||
<ComponentsList on:reload={fetchCustomComponents} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<section class="p-2 sticky w-full z-10 top-0 bg-surface">
|
||||
<ClearableInput bind:value={search} placeholder="Search components..." />
|
||||
</section>
|
||||
|
||||
@@ -170,7 +247,7 @@
|
||||
<ListItem title={'Groups'}>
|
||||
<div class="flex flex-wrap gap-3 py-2">
|
||||
{#if groups}
|
||||
{#each groups as group (group)}
|
||||
{#each groups as group (group.path)}
|
||||
<div class="w-20">
|
||||
<button
|
||||
on:click={() => {
|
||||
@@ -188,6 +265,59 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="w-20">
|
||||
<button
|
||||
on:click={() => {
|
||||
addNewGroup()
|
||||
}}
|
||||
title=""
|
||||
class="transition-all border w-20 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
justify-center bg-surface rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 duration-200 hover:border-blue-500"
|
||||
>
|
||||
<Plus class="text-secondary" />
|
||||
</button>
|
||||
<div class="text-xs text-center flex-wrap text-secondary mt-1"> Add new </div>
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem title={'Custom Components'} tooltip={'Create components in React or vanilla JS'}>
|
||||
<div class="flex flex-wrap gap-3 py-2">
|
||||
{#if customComponents}
|
||||
{#each customComponents as cc (cc.path)}
|
||||
<div class="w-20">
|
||||
<button
|
||||
on:click={() => {
|
||||
addCustomComponent(cc)
|
||||
}}
|
||||
title={cc.name}
|
||||
class="transition-all border w-20 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
justify-center bg-surface rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 duration-200 hover:border-blue-500"
|
||||
>
|
||||
<LayoutDashboard class="text-secondary" />
|
||||
</button>
|
||||
<div class="text-xs text-center flex-wrap text-secondary mt-1">
|
||||
{cc.name}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="w-20">
|
||||
<button
|
||||
on:click={() => {
|
||||
if (!$enterpriseLicense) {
|
||||
sendUserToast('Custom components are only available on the EE', true)
|
||||
} else {
|
||||
ccDrawer.openDrawer()
|
||||
}
|
||||
}}
|
||||
title=""
|
||||
class="transition-all border w-20 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
justify-center bg-surface rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 duration-200 hover:border-blue-500"
|
||||
>
|
||||
<Plus class="text-secondary" />
|
||||
</button>
|
||||
<div class="text-xs text-center flex-wrap text-secondary mt-1"> Add new </div>
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
|
||||
import { Trash } from 'lucide-svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import NameEditor from './NameEditor.svelte'
|
||||
|
||||
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { ResourceService } from '$lib/gen/services/ResourceService'
|
||||
|
||||
export let row: {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function toggleDelete() {
|
||||
if ($workspaceStore) {
|
||||
await ResourceService.deleteResource({
|
||||
workspace: $workspaceStore,
|
||||
path: row.path
|
||||
})
|
||||
}
|
||||
dispatch('reload')
|
||||
sendUserToast('Component deleted:\n' + row.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<Cell first>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<NameEditor
|
||||
kind="custom component"
|
||||
on:update={async (e) => {
|
||||
if (!$workspaceStore) return
|
||||
const cc = await ResourceService.getResourceValue({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: row.path
|
||||
})
|
||||
await ResourceService.updateResource({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: row.path,
|
||||
requestBody: {
|
||||
path: `f/app_custom/${e.detail.name.replace(/-/g, '_').replace(/\s/g, '_')}`,
|
||||
value: {
|
||||
...cc,
|
||||
name: e.detail.name
|
||||
}
|
||||
}
|
||||
})
|
||||
dispatch('reload')
|
||||
|
||||
sendUserToast('Component name updated:\n' + e.detail.name)
|
||||
}}
|
||||
{row}
|
||||
/>
|
||||
{row.name}
|
||||
</div>
|
||||
</Cell>
|
||||
|
||||
<Cell last>
|
||||
<div class={twMerge('flex flex-row gap-1 justify-end z-[10000]')}>
|
||||
<button on:pointerdown|stopPropagation>
|
||||
<ButtonDropdown target="#cc_portal" hasPadding={false}>
|
||||
<svelte:fragment slot="items">
|
||||
<MenuItem on:click={toggleDelete}>
|
||||
<div
|
||||
class={classNames(
|
||||
'!text-red-600 flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-gray-100 !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
Delete
|
||||
</div>
|
||||
</MenuItem>
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
</button>
|
||||
</div>
|
||||
</Cell>
|
||||
</tr>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import CustomComponentRow from './CustomComponentRow.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
let customcomponents: Array<{
|
||||
name: string
|
||||
path: string
|
||||
}> = []
|
||||
|
||||
let loading: boolean = false
|
||||
|
||||
async function getCustomComponents() {
|
||||
loading = true
|
||||
customcomponents = await ResourceService.listResourceNames({
|
||||
workspace: $workspaceStore ?? '',
|
||||
name: 'app_custom'
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function addCustomComponent(nameField: string) {
|
||||
try {
|
||||
const message = await ResourceService.createResource({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
resource_type: 'app_custom',
|
||||
path: `f/app_custom/${nameField.replace(/-/g, '_').replace(/\s/g, '_')}`,
|
||||
value: {
|
||||
value: {
|
||||
additionalLibs: {
|
||||
reactVersion: useReact ? reactVersion : undefined
|
||||
}
|
||||
},
|
||||
name: nameField,
|
||||
js: await files[0].text()
|
||||
}
|
||||
}
|
||||
})
|
||||
sendUserToast('Component created: ' + message)
|
||||
dispatch('reload')
|
||||
} catch (e) {
|
||||
sendUserToast(
|
||||
'Component creation failed. You need write privilege on folder app_custom: ' + e.body ?? e,
|
||||
true
|
||||
)
|
||||
}
|
||||
getCustomComponents()
|
||||
nameField = ''
|
||||
}
|
||||
|
||||
let nameField: string = ''
|
||||
let reactVersion: string = '18.2.0'
|
||||
let files: FileList
|
||||
let useReact = true
|
||||
|
||||
getCustomComponents()
|
||||
</script>
|
||||
|
||||
<div id="cc_portal" />
|
||||
|
||||
<div class="p-2 flex flex-col items-start w-auto gap-2 relative">
|
||||
<div class="w-full flex flex-col gap-y-2 pb-8">
|
||||
<div>
|
||||
<input type="text" bind:value={nameField} placeholder={'Custom Component name'} />
|
||||
</div>
|
||||
<div>
|
||||
<input type="file" accept="text/javascript " bind:files multiple={false} />
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Toggle bind:checked={useReact} options={{ right: 'React Component' }} />
|
||||
<div>
|
||||
<input
|
||||
disabled={!useReact}
|
||||
type="text"
|
||||
bind:value={reactVersion}
|
||||
placeholder={'React version'}
|
||||
/></div
|
||||
></div
|
||||
>
|
||||
<Button on:click={() => addCustomComponent(nameField)} color="dark" size="xs"
|
||||
>Add Custom Component</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex flex-col w-full pt-12">
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[2], 0.5]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if Array.isArray(customcomponents) && customcomponents.length > 0}
|
||||
<div class="w-full">
|
||||
<DataTable size="xs">
|
||||
<Head>
|
||||
<tr>
|
||||
<Cell first head>Path</Cell>
|
||||
<Cell last head />
|
||||
</tr>
|
||||
</Head>
|
||||
<tbody class="divide-y">
|
||||
{#if customcomponents && customcomponents.length > 0}
|
||||
{#each customcomponents as row}
|
||||
{#key row}
|
||||
<CustomComponentRow
|
||||
{row}
|
||||
on:reload={() => {
|
||||
getCustomComponents()
|
||||
dispatch('reload')
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/each}
|
||||
{:else}
|
||||
<tr>Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { listGroups, type Group, createGroup } from './groupUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
@@ -11,8 +11,11 @@
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import GroupRow from './GroupRow.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import type { AppViewerContext, GridItem } from '../../types'
|
||||
import { getAllSubgridsAndComponentIds } from '../appUtils'
|
||||
|
||||
export let selectedGroup: object = {}
|
||||
export let item: GridItem
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let groups: Array<{
|
||||
name: string
|
||||
@@ -27,36 +30,52 @@
|
||||
loading = false
|
||||
}
|
||||
|
||||
function getSubgrids(item: GridItem) {
|
||||
let allSubgrids = {}
|
||||
let subgrids = getAllSubgridsAndComponentIds($app, item.data)[0]
|
||||
for (let key of subgrids) {
|
||||
allSubgrids[key] = $app.subgrids?.[key]
|
||||
}
|
||||
return allSubgrids
|
||||
}
|
||||
async function addGroup(nameField: string) {
|
||||
const groups = await ResourceService.listResourceNames({
|
||||
workspace: $workspaceStore!,
|
||||
name: 'app_group'
|
||||
})
|
||||
|
||||
let subgrids = getSubgrids(item)
|
||||
|
||||
const group: Group = {
|
||||
path: 'f/app_groups/group_' + groups.length,
|
||||
value: {
|
||||
value: selectedGroup,
|
||||
value: { item, subgrids },
|
||||
name: nameField
|
||||
}
|
||||
}
|
||||
|
||||
const message = await createGroup($workspaceStore!, group)
|
||||
try {
|
||||
const message = await createGroup($workspaceStore!, group)
|
||||
|
||||
sendUserToast('Group created: ' + message)
|
||||
} catch (e) {
|
||||
sendUserToast(
|
||||
'Group creation failed. You need write privilege on folder app_groups: ' + e.body ?? e,
|
||||
true
|
||||
)
|
||||
}
|
||||
getGroups()
|
||||
|
||||
nameField = ''
|
||||
|
||||
sendUserToast('Group created:' + message)
|
||||
}
|
||||
|
||||
let nameField: string = ''
|
||||
|
||||
onMount(() => {
|
||||
getGroups()
|
||||
})
|
||||
getGroups()
|
||||
</script>
|
||||
|
||||
<div id="group_portal" />
|
||||
|
||||
<div class="p-2 flex flex-col items-start w-auto gap-2 relative">
|
||||
<div class="w-full flex flex-row gap-2 items-center">
|
||||
<input bind:value={nameField} placeholder={'Group name'} />
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Drawer, DrawerContent } from '$lib/components/common'
|
||||
import GroupList from './GroupList.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext, GridItem } from '../../types'
|
||||
import { getAllSubgridsAndComponentIds } from '../appUtils'
|
||||
import type { GridItem } from '../../types'
|
||||
|
||||
export let item: GridItem
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: subgrids = getSubgrids(item)
|
||||
|
||||
function getSubgrids(item: GridItem) {
|
||||
let allSubgrids = {}
|
||||
let subgrids = getAllSubgridsAndComponentIds($app, item.data)[0]
|
||||
for (let key of subgrids) {
|
||||
allSubgrids[key] = $app.subgrids?.[key]
|
||||
}
|
||||
return allSubgrids
|
||||
}
|
||||
|
||||
let codeDrawer: Drawer
|
||||
|
||||
export async function openDrawer() {
|
||||
@@ -28,12 +13,7 @@
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={codeDrawer}>
|
||||
<DrawerContent title="Group manager" on:close={codeDrawer.closeDrawer}>
|
||||
<GroupList
|
||||
selectedGroup={{
|
||||
item,
|
||||
subgrids
|
||||
}}
|
||||
/>
|
||||
<DrawerContent title="Component Groups" on:close={codeDrawer.closeDrawer}>
|
||||
<GroupList {item} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { deleteGroup } from './groupUtils'
|
||||
import { deleteGroup, getGroup, updateGroup } from './groupUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import GroupNameEditor from './GroupNameEditor.svelte'
|
||||
import GroupNameEditor from './NameEditor.svelte'
|
||||
|
||||
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
@@ -32,15 +32,31 @@
|
||||
<tr>
|
||||
<Cell first>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<GroupNameEditor on:reloadGroups {row} />
|
||||
<GroupNameEditor
|
||||
kind="group"
|
||||
on:update={async (e) => {
|
||||
if (!$workspaceStore) return
|
||||
const group = await getGroup($workspaceStore, row.path)
|
||||
await updateGroup($workspaceStore, row.path, {
|
||||
value: {
|
||||
...group,
|
||||
name: e.detail.name
|
||||
}
|
||||
})
|
||||
dispatch('reloadGroups')
|
||||
|
||||
sendUserToast('Group name updated:\n' + e.detail.name)
|
||||
}}
|
||||
{row}
|
||||
/>
|
||||
{row.name}
|
||||
</div>
|
||||
</Cell>
|
||||
|
||||
<Cell last>
|
||||
<div class={twMerge('flex flex-row gap-1 justify-end ')}>
|
||||
<div class={twMerge('flex flex-row gap-1 justify-end z-[10000]')}>
|
||||
<button on:pointerdown|stopPropagation>
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<ButtonDropdown target="#group_portal" hasPadding={false}>
|
||||
<svelte:fragment slot="items">
|
||||
<MenuItem on:click={toggleDelete}>
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import { isOpenStore } from './store'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
export let title: string
|
||||
export let prefix: string | undefined = undefined
|
||||
@@ -11,6 +12,8 @@
|
||||
export let toggleClasses = ''
|
||||
export let contentWrapperClasses = ''
|
||||
export let isOpen = false
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let documentationLink: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -37,6 +40,13 @@
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
{#if tooltip}
|
||||
<Tooltip
|
||||
class="ml-1"
|
||||
documentationLink="https://github.com/windmill-labs/windmill-custom-component-template"
|
||||
>{tooltip}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
</h1>
|
||||
<ChevronDown class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
|
||||
</button>
|
||||
@@ -50,6 +60,13 @@
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
{#if tooltip}
|
||||
<Tooltip
|
||||
class="ml-1"
|
||||
documentationLink="https://github.com/windmill-labs/windmill-custom-component-template"
|
||||
>{tooltip}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="px-2">
|
||||
<slot />
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getGroup, updateGroup } from './groupUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
|
||||
import { Popup } from '$lib/components/common'
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let kind: string
|
||||
export let row: {
|
||||
name: string
|
||||
path: string
|
||||
@@ -16,6 +14,11 @@
|
||||
let editedName = row.name
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
function onkeydown(e) {
|
||||
if (e.key === 'Enter') {
|
||||
dispatch('update', { path: row.path, name: editedName })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
@@ -31,25 +34,13 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-80 gap-2">
|
||||
<div class="leading-6 font-semibold text-xs">Edit group name</div>
|
||||
<div class="leading-6 font-semibold text-xs">Edit {kind} name</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<input bind:value={editedName} />
|
||||
<input on:keydown={onkeydown} bind:value={editedName} />
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
if (!$workspaceStore) return
|
||||
const group = await getGroup($workspaceStore, row.path)
|
||||
await updateGroup($workspaceStore, row.path, {
|
||||
value: {
|
||||
...group,
|
||||
name: editedName
|
||||
}
|
||||
})
|
||||
dispatch('reloadGroups')
|
||||
close(null)
|
||||
sendUserToast('Group name updated:\n' + editedName)
|
||||
}}
|
||||
on:click={() => dispatch('update', { path: row.path, name: editedName }) && close(null)}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
@@ -559,6 +559,7 @@ export const quickStyleProperties: Record<
|
||||
container: containerDefaultProps,
|
||||
button: buttonDefaultProps
|
||||
},
|
||||
customcomponent: {},
|
||||
htmlcomponent: {
|
||||
container: [
|
||||
layoutGrouping,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Portal from 'svelte-portal'
|
||||
|
||||
export let hasPadding: boolean = true
|
||||
|
||||
export let target: string | undefined = undefined
|
||||
const [popperRef, popperContent] = createPopperActions({ placement: 'auto' })
|
||||
|
||||
const popperOptions: PopperOptions<{}> = {
|
||||
@@ -38,7 +38,7 @@
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
<Portal>
|
||||
<Portal {target}>
|
||||
<div use:popperContent={popperOptions} class="z-[6000]">
|
||||
<Transition
|
||||
show={open}
|
||||
|
||||
2
frontend/static/cc.iife.js
Normal file
2
frontend/static/cc.iife.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var o=document.createElement("style");o.appendChild(document.createTextNode(".mycomponent{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#999;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}.mycomponent a{font-weight:500;color:#646cff;text-decoration:inherit}.mycomponent a:hover{color:#535bf2}.mycomponent h1{font-size:3.2em;line-height:1.1}.mycomponent button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}.mycomponent button:hover{border-color:#646cff}.mycomponent>button:focus,.mycomponent>button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media (prefers-color-scheme: light){.mycomponent{color:#213547;background-color:#fff}.mycomponent a:hover{color:#747bff}.mycomponent button{background-color:#f9f9f9}}.mycomponent{margin:0 auto;text-align:center}.mycomponent .card{padding:2em}")),document.head.appendChild(o)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
|
||||
var wmc=function(t,e,r){"use strict";const w="";function i({passSetters:n,setOutput:m,renderInit:u}){const[d,l]=e.useState(u),[s,c]=e.useState(void 0);return e.useEffect(()=>{n({onInput:a=>{c(a)},onRender:l})},[l,n]),d?e.createElement(e.Fragment,null,e.createElement("h1",null,"Minimal Component"),e.createElement("pre",null,JSON.stringify(s)),e.createElement("button",{style:{fontSize:"12px",marginBottom:"10px"},onClick:()=>{m("hello world")}},"set output")):e.createElement(e.Fragment,null)}function o(n){r.createRoot(document.getElementById(n.id)).render(e.createElement(e.StrictMode,null,e.createElement(i,{passSetters:n.passSetters,setOutput:n.setOutput,renderInit:n.render})))}return window.windmill===void 0&&(window.windmill={}),window.windmill.mycomponent=o,t.customComponent=o,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),t}({},React,ReactDOM);
|
||||
Reference in New Issue
Block a user