feat(frontend): Fix component synchro (#1038)

* feat(frontend): Fix component synchro

* feat(frontend): Fix isObject
This commit is contained in:
Faton Ramadani
2022-12-21 11:19:27 +01:00
committed by GitHub
parent fa1b10a6b5
commit 611d90f7cb
15 changed files with 190 additions and 241 deletions
@@ -130,7 +130,7 @@
})
if (previewJobUpdates.new_logs) {
job.logs = (job.logs ?? '').concat(previewJobUpdates.new_logs)
job.logs = (job?.logs ?? '').concat(previewJobUpdates.new_logs)
}
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
job = await JobService.getJob({ workspace: workspace!, id })
@@ -49,7 +49,8 @@
const options = {
responsive: true,
animation: false
animation: false,
maintainAspectRatio: false
}
$: data = {
@@ -14,7 +14,7 @@
let result: string = ''
</script>
<RunnableWrapper bind:componentInput {id} bind:result autoRefresh={false}>
<RunnableWrapper bind:componentInput {id} bind:result>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if result === ''}
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
@@ -32,18 +32,13 @@
<InputValue input={configuration.maxDate} bind:value={maxValue} />
<AlignWrapper {verticalAlignment}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="w-full">
<div>
{labelValue}
</div>
<input
type={inputType}
bind:this={input}
on:input={handleInput}
min={minValue}
max={maxValue}
placeholder="Type..."
/>
</label>
<input
type={inputType}
bind:this={input}
on:input={handleInput}
min={minValue}
max={maxValue}
placeholder="Type..."
class="h-full"
/>
</AlignWrapper>
@@ -36,7 +36,7 @@
let testIsLoading = false
let runnableInputValues: Record<string, any> = {}
$: mergedArgs = { ...args, ...extraQueryParams, ...runnableInputValues }
$: mergedArgs = { ...extraQueryParams, ...runnableInputValues, ...args }
function setStaticInputsToArgs() {
Object.entries(inputs ?? {}).forEach(([key, value]) => {
@@ -51,9 +51,13 @@
$: inputs && setStaticInputsToArgs()
function argMergedArgsValid(mergedArgs: Record<string, any>, testJobLoader) {
if (!inputs) {
return false
}
if (
Object.keys(inputs ?? {}).filter((k) => inputs[k].type !== 'user').length !==
Object.keys(runnableInputValues).length
Object.keys(inputs).length !==
Object.keys(mergedArgs).length - Object.keys(extraQueryParams).length
) {
return false
}
@@ -211,22 +215,24 @@
bind:this={testJobLoader}
/>
{#if schemaStripped !== undefined && (autoRefresh || forceSchemaDisplay)}
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
{/if}
{#if !runnable && autoRefresh}
<Alert type="warning" size="xs" class="mt-2" title="Missing runnable">
Please select a runnable
</Alert>
{:else if autoRefresh === true}
{#if isValid}
<slot />
{:else}
<Alert type="warning" size="xs" class="mt-2" title="Missing inputs">
Please fill in all the inputs
</Alert>
<div class="h-full flex flex-col">
{#if schemaStripped !== undefined && (autoRefresh || forceSchemaDisplay)}
<SchemaForm schema={schemaStripped} bind:args {isValid} {disabledArgs} shouldHideNoInputs />
{/if}
{:else}
<slot />
{/if}
{#if !runnable && autoRefresh}
<Alert type="warning" size="xs" class="mt-2" title="Missing runnable">
Please select a runnable
</Alert>
{:else if autoRefresh === true}
{#if isValid}
<slot />
{:else}
<Alert type="warning" size="xs" class="mt-2" title="Missing inputs">
Please fill in all the inputs
</Alert>
{/if}
{:else}
<slot />
{/if}
</div>
@@ -19,7 +19,7 @@
$: outputs = $worldStore?.outputsById[id] as {
result: Output<number | null>
}
$: if(value || !value) {
$: if (value || !value) {
// Disallow 'e' character in numbers
// if(value && value.toString().includes('e')) {
// value = +value.toString().replaceAll('e', '')
@@ -32,18 +32,12 @@
<InputValue input={configuration.label} bind:value={labelValue} />
<AlignWrapper {verticalAlignment}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="w-full">
<div>
{labelValue}
</div>
<DebouncedInput
bind:value={value}
debounceDelay={300}
type="number"
inputmode="numeric"
pattern="\d*"
placeholder="Type..."
/>
</label>
<input
bind:value
type="number"
inputmode="numeric"
pattern="\d*"
placeholder="Type..."
class="h-full"
/>
</AlignWrapper>
@@ -17,12 +17,12 @@
let label: string
let items: string[]
let itemKey: string
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string | undefined>
}
function onChange({detail}: CustomEvent) {
function onChange({ detail }: CustomEvent) {
outputs?.result.set(detail?.[itemKey] || undefined)
}
</script>
@@ -32,20 +32,5 @@
<InputValue input={configuration.itemKey} bind:value={itemKey} />
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block w-full">
<div>
{label}
</div>
<div>
<Select
on:clear={onChange}
on:change={onChange}
{items}
class="w-full"
placeholder="Select an item"
/>
</div>
</label>
<Select on:clear={onChange} on:change={onChange} {items} placeholder="Select an item" />
</AlignWrapper>
@@ -8,11 +8,12 @@
import { writable } from 'svelte/store'
import { createSvelteTable, flexRender, type TableOptions } from '@tanstack/svelte-table'
import AppButton from '../buttons/AppButton.svelte'
import { classNames } from '$lib/utils'
import { classNames, isObject } from '$lib/utils'
import DebouncedInput from '../helpers/DebouncedInput.svelte'
import AppTableFooter from './AppTableFooter.svelte'
import RefreshButton from '../helpers/RefreshButton.svelte'
import { tableOptions } from './tableOptions'
import Alert from '$lib/components/common/alert/Alert.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -102,79 +103,85 @@
<InputValue input={configuration.search} bind:value={search} />
<InputValue input={configuration.pagination} bind:value={pagination} />
<RunnableWrapper bind:componentInput {id} bind:result {extraQueryParams} autoRefresh={false}>
<div class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full">
<div class="py-2 px-4">
<div class="flex justify-between items-center">
<RefreshButton componentId={id} />
{#if search !== 'Disabled'}
<div>
<DebouncedInput placeholder="Search..." bind:value={searchValue} />
</div>
{/if}
<RunnableWrapper bind:componentInput {id} bind:result {extraQueryParams}>
{#if Array.isArray(result) && result.every(isObject)}
<div class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full">
<div class="py-2 px-4">
<div class="flex justify-between items-center">
<RefreshButton componentId={id} />
{#if search !== 'Disabled'}
<div>
<DebouncedInput placeholder="Search..." bind:value={searchValue} />
</div>
{/if}
</div>
</div>
</div>
<div class="overflow-auto flex-1 w-full">
<table class="divide-y divide-gray-300 w-full border-b border-b-gray-200">
<thead class="bg-gray-50 text-left">
{#each $table.getHeaderGroups() as headerGroup}
<tr class="divide-x">
{#each headerGroup.headers as header}
<th class="px-4 py-4 text-sm font-semibold">
{#if !header.isPlaceholder}
<div class="overflow-auto flex-1 w-full">
<table class="divide-y divide-gray-300 w-full border-b border-b-gray-200">
<thead class="bg-gray-50 text-left">
{#each $table.getHeaderGroups() as headerGroup}
<tr class="divide-x">
{#each headerGroup.headers as header}
<th class="px-4 py-4 text-sm font-semibold">
{#if !header.isPlaceholder}
<svelte:component
this={flexRender(header.column.columnDef.header, header.getContext())}
/>
{/if}
</th>
{/each}
{#if actionButtons.length > 0}
<th class="px-4 py-4 text-sm font-semibold">Actions</th>
{/if}
</tr>
{/each}
</thead>
<tbody class="divide-y divide-gray-200 bg-white ">
{#each $table.getRowModel().rows as row, rowIndex (row.id)}
<tr
class={classNames(
selectedRowIndex === rowIndex
? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-50',
'divide-x',
'border-b w-full',
selectedRowIndex === rowIndex
? 'divide-blue-200 hover:divide-blue-300'
: 'divide-gray-200'
)}
on:click={() => toggleRow(row, rowIndex)}
>
{#each row.getVisibleCells() as cell, index (index)}
<td class="p-4 whitespace-nowrap text-xs text-gray-900">
<svelte:component
this={flexRender(header.column.columnDef.header, header.getContext())}
this={flexRender(cell.column.columnDef.cell, cell.getContext())}
/>
{/if}
</th>
{/each}
{#if actionButtons.length > 0}
<th class="px-4 py-4 text-sm font-semibold">Actions</th>
{/if}
</tr>
{/each}
</thead>
<tbody class="divide-y divide-gray-200 bg-white ">
{#each $table.getRowModel().rows as row, rowIndex (row.id)}
<tr
class={classNames(
selectedRowIndex === rowIndex
? 'bg-blue-100 hover:bg-blue-200'
: 'hover:bg-blue-50',
'divide-x',
'border-b w-full',
selectedRowIndex === rowIndex
? 'divide-blue-200 hover:divide-blue-300'
: 'divide-gray-200'
)}
on:click={() => toggleRow(row, rowIndex)}
>
{#each row.getVisibleCells() as cell, index (index)}
<td class="p-4 whitespace-nowrap text-xs text-gray-900">
<svelte:component
this={flexRender(cell.column.columnDef.cell, cell.getContext())}
/>
</td>
{/each}
</td>
{/each}
{#if actionButtons.length > 0}
<td class="flex flex-row gap-2 p-4">
{#each actionButtons as props, actionIndex (actionIndex)}
<AppButton
{...props}
extraQueryParams={{ row }}
bind:componentInput={props.componentInput}
bind:staticOutputs={$staticOutputsStore[props.id]}
/>
{/each}
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
{#if actionButtons.length > 0}
<td class="flex flex-row gap-2 p-4">
{#each actionButtons as props, actionIndex (actionIndex)}
<AppButton
{...props}
extraQueryParams={{ row }}
bind:componentInput={props.componentInput}
bind:staticOutputs={$staticOutputsStore[props.id]}
/>
{/each}
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
</div>
<AppTableFooter paginationEnabled={pagination} {result} {table} />
</div>
<AppTableFooter paginationEnabled={pagination} {result} {table} />
</div>
{:else}
<Alert title="Parsing issues" type="error" size="xs">
The result should be an array of objects
</Alert>
{/if}
</RunnableWrapper>
@@ -28,16 +28,11 @@
<InputValue input={configuration.label} bind:value={labelValue} />
<AlignWrapper {verticalAlignment}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="w-full">
<div>
{labelValue}
</div>
<input
type={inputType}
bind:this={input}
on:input={handleInput}
placeholder="Type..."
/>
</label>
<input
type={inputType}
bind:this={input}
on:input={handleInput}
placeholder="Type..."
class="h-full"
/>
</AlignWrapper>
@@ -4,17 +4,9 @@
import Grid from 'svelte-grid'
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
import {
columnConfiguration,
disableDrag,
enableDrag,
gridColumns,
isFixed,
toggleFixed
} from '../gridUtils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import { Alert } from '$lib/components/common'
import { fly } from 'svelte/transition'
import gridHelp from 'svelte-grid/build/helper/index.mjs'
import Button from '$lib/components/common/button/Button.svelte'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
@@ -92,16 +84,17 @@
<ComponentEditor
bind:component={gridComponent.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(gridComponent)}
on:delete={() => removeGridElement(gridComponent.data)}
on:lock={() => {
gridComponent = toggleFixed(gridComponent)
}}
locked={isFixed(gridComponent)}
/>
</div>
{/if}
{/each}
</Grid>
{#if $connectingInput.opened}
<div
class="fixed top-32 z-10 flex justify-center items-center"
@@ -19,7 +19,7 @@
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
const dimensions: Record<`${number}:${number}-${number}:${number}`, AppComponent['type'][]> = {
'1:1-3:1': ['buttoncomponent', 'textcomponent', 'checkboxcomponent'],
'1:2-3:2': ['textinputcomponent', 'numberinputcomponent', 'selectcomponent'],
'1:2-2:1': ['textinputcomponent', 'numberinputcomponent', 'selectcomponent'],
'2:2-6:4': ['displaycomponent'],
'2:3-6:4': ['formcomponent'],
'2:4-6:4': ['barchartcomponent', 'piechartcomponent'],
@@ -33,6 +33,21 @@
return { w: +size[0], h: +size[1] }
}
function getMaxDimensionsByComponent(componentType: AppComponent['type'], column: number): Size {
if (
[
'textinputcomponent',
'numberinputcomponent',
'selectcomponent',
'dateinputcomponent',
'passwordinputcomponent'
].includes(componentType)
) {
return { w: column, h: 1 }
}
return { w: column, h: 12 }
}
function addComponent(appComponent: AppComponent) {
const grid = $app.grid ?? []
const id = getNextId(grid.map((gridItem) => gridItem.data.id))
@@ -56,7 +71,7 @@
gridColumns.forEach((column) => {
const min = getMinDimensionsByComponent(appComponent.type, column)
const max = { w: 12, h: 12 }
const max = getMaxDimensionsByComponent(appComponent.type, column)
newItem[column] = { ...newComponent, min, max, w: min.w, h: min.h }
const position = gridHelp.findSpace(newItem, grid, column)
@@ -9,15 +9,7 @@ const inputs: ComponentSet = {
id: 'textinputcomponent',
type: 'textinputcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
visible: false,
value: 'Label',
fieldType: 'textarea',
defaultValue: 'Label'
}
},
configuration: {},
card: false
},
{
@@ -25,15 +17,7 @@ const inputs: ComponentSet = {
id: 'passwordinputcomponent',
type: 'passwordinputcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
visible: false,
value: 'Label',
fieldType: 'textarea',
defaultValue: 'Label'
}
},
configuration: {},
card: false
},
{
@@ -41,15 +25,7 @@ const inputs: ComponentSet = {
id: 'numberinputcomponent',
type: 'numberinputcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
visible: false,
value: 'Label',
fieldType: 'textarea',
defaultValue: 'Label'
}
},
configuration: {},
card: false
},
{
@@ -58,13 +34,6 @@ const inputs: ComponentSet = {
type: 'dateinputcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
visible: false,
value: 'Title',
fieldType: 'textarea',
defaultValue: 'Title'
},
minDate: {
type: 'static',
visible: false,
@@ -78,7 +47,7 @@ const inputs: ComponentSet = {
value: '',
fieldType: 'date',
defaultValue: ''
},
}
},
card: false,
softWrap: true
@@ -105,13 +74,6 @@ const inputs: ComponentSet = {
type: 'selectcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
visible: false,
value: 'Label',
fieldType: 'textarea',
defaultValue: 'Label'
},
items: {
type: 'static',
fieldType: 'array',
@@ -64,8 +64,8 @@
const { componentInput } = gridItem.data
if (
componentInput.type === 'runnable' &&
componentInput.runnable?.type === 'runnableByName'
componentInput?.type === 'runnable' &&
componentInput?.runnable?.type === 'runnableByName'
) {
acc.push(componentInput.runnable.name)
}
@@ -61,7 +61,6 @@ export interface BaseAppComponent extends Partial<Aligned> {
* *For example when the component has a popup like `Select`*
*/
softWrap?: boolean
// TODO: add min/max width/height
}
export type AppComponent = BaseAppComponent &
+27 -30
View File
@@ -1,6 +1,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { goto } from '$app/navigation'
import { FlowService, FolderService, Script, ScriptService, type Flow, type FlowModule, type User } from '$lib/gen'
import {
FlowService,
FolderService,
Script,
ScriptService,
type Flow,
type FlowModule,
type User
} from '$lib/gen'
import { toast } from '@zerodevx/svelte-toast'
import type { Schema, SupportedLanguage } from './common'
import { hubScripts, workspaceStore, type UserExt } from './stores'
@@ -60,12 +68,13 @@ export function displayDate(dateString: string | undefined, displaySecond = fals
if (date.toString() === 'Invalid Date') {
return ''
} else {
return `${date.getFullYear()}/${date.getMonth() + 1
}/${date.getDate()} at ${date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: displaySecond ? '2-digit' : undefined
})}`
return `${date.getFullYear()}/${
date.getMonth() + 1
}/${date.getDate()} at ${date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: displaySecond ? '2-digit' : undefined
})}`
}
}
@@ -175,11 +184,7 @@ export function removeItemAll<T>(arr: T[], value: T) {
return arr
}
export async function isOwner(
path: string,
user: UserExt,
workspace: string
): Promise<boolean> {
export async function isOwner(path: string, user: UserExt, workspace: string): Promise<boolean> {
if (isObviousOwner(path, user)) {
return true
} else if (path.startsWith('f/')) {
@@ -191,11 +196,7 @@ export async function isOwner(
}
}
export function isObviousOwner(
path: string,
user?: UserExt
): boolean {
export function isObviousOwner(path: string, user?: UserExt): boolean {
if (!user) {
return false
}
@@ -206,14 +207,10 @@ export function isObviousOwner(
if (path.startsWith(userOwner)) {
return true
}
if (
user.pgroups.findIndex((x) => path.startsWith(x)) != -1
) {
if (user.pgroups.findIndex((x) => path.startsWith(x)) != -1) {
return true
}
if (
user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1
) {
if (user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1) {
return true
}
return false
@@ -235,14 +232,10 @@ export function canWrite(
if (keys.includes(userOwner) && extra_perms[userOwner]) {
return true
}
if (
user.pgroups.findIndex((x) => keys.includes(x) && extra_perms[x]) != -1
) {
if (user.pgroups.findIndex((x) => keys.includes(x) && extra_perms[x]) != -1) {
return true
}
if (
user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1
) {
if (user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1) {
return true
}
return false
@@ -378,7 +371,7 @@ export function mapUserToUserExt(user: User): UserExt {
return {
...user,
groups: user.groups!,
pgroups: user.groups!.map((x) => `g/${x}`),
pgroups: user.groups!.map((x) => `g/${x}`)
}
}
@@ -631,3 +624,7 @@ export function capitalize(word: string): string {
export function isCloudHosted(): boolean {
return get(page).url.hostname == 'app.windmill.dev'
}
export function isObject(obj: any) {
return typeof obj === 'object'
}