Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shuffle things around #17

Merged
merged 18 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
with:
node-version: 16.x
cache: 'yarn'
- run: yarn
- run: yarn
- run: yarn test
- run: yarn lint || true # TODO dont ignore lint errors
- run: yarn lint
- run: yarn build
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,27 @@ The configuration file contains paths to input files which are included in the z
The workflow configuration file consists out of 2 parts:

1. Global parameters, which are available to engine and each node.
2. Table with parameters for each node the workflow should run.
2. Tables with parameters for each node the workflow should run.

TOML does not allow for tables with same name. So any node that needs be run multiple times should have a index appened to the table name.
TOML does not allow for tables with same name. So any node that occurs multiple times should have a index appened to the table name.

### Catalog

The catalog is a YAML formatted file which tells the app what nodes are available. In has the following info:

1. Description of global parameters
1. global: Description of global parameters
* schema: What parameters are valid. Formatted as JSON schema draft 7.
* uiSchema: How the form for filling the parameters should be rendered.
2. Description of available nodes.
2. nodes: Description of available nodes.
* id: Identifier of node, for computers
* label: Label of node, for humans
* category: Category to which node belongs
* description: Text describing what node needs, does and produces.
* schema: What parameters are valid. Formatted as JSON schema draft 7.
* uiSchema: How the form for filling the parameters should be rendered.
3. Descriptions of node categories
4. Title and link to example workflows
5. Title of the catalog
3. catagories: Descriptions of node categories
* name: Name of category
* description: Description of category
4. examples: Title and link to example workflows
* map with title as key and link as value
5. title: Title of the catalog
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,10 @@
},
"engines": {
"node": "~16"
},
"ts-standard": {
"ignore": [
"vite.config.ts"
]
}
}
23 changes: 13 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,25 @@ import 'react-toastify/dist/ReactToastify.css'

import './App.css'
import { CatalogPanel } from './CatalogPanel'
import { StepPanel } from './StepPanel'
import { NodePanel } from './NodePanel'
import { WorkflowPanel } from './WorkflowPanel'
import { Header } from './Header'
import { ErrorBoundary } from './ErrorBoundary'

function App (): JSX.Element {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<ToastContainer position='top-center' autoClose={1000} closeOnClick pauseOnFocusLoss />
<Header />
<div style={{ display: 'grid', gridTemplateColumns: '400px 0.6fr 1fr', gridAutoRows: '90vh', columnGap: '0.5em' }}>
<CatalogPanel />
<WorkflowPanel />
<StepPanel />
</div>
</React.Suspense>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<ToastContainer position='top-center' autoClose={1000} closeOnClick pauseOnFocusLoss />
<Header />
<div style={{ display: 'grid', gridTemplateColumns: '400px 0.6fr 1fr', gridAutoRows: '90vh', columnGap: '0.5em' }}>
<CatalogPanel />
<WorkflowPanel />
<NodePanel />
</div>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/CatalogCategory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCatalog } from './store'
import { ICategory } from './types'
import { CatalogNode } from './CatalogNode'

export const CatalogCategory = ({ name }: ICategory) => {
export const CatalogCategory = ({ name }: ICategory): JSX.Element => {
const catalog = useCatalog()
return (
<li>
Expand Down
4 changes: 2 additions & 2 deletions src/CatalogNode.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useWorkflow } from './store'
import { INode } from './types'
import { ICatalogNode } from './types'

export const CatalogNode = ({ id, label }: INode) => {
export const CatalogNode = ({ id, label }: ICatalogNode): JSX.Element => {
const { addNodeToWorkflow } = useWorkflow()
return (
<li>
Expand Down
9 changes: 5 additions & 4 deletions src/CatalogPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import React from 'react'

import { CatalogPicker } from './CatalogPicker'
import { CatalogCategory } from './CatalogCategory'
import { useCatalog, useWorkflow } from './store'
import { Example } from './Example'

export const CatalogPanel = () => {
export const CatalogPanel = (): JSX.Element => {
const catalog = useCatalog()
const { toggleGlobalEdit } = useWorkflow()
return (
<fieldset>
<legend>Step catalog</legend>
<legend>Catalog</legend>
<CatalogPicker />
<React.Suspense fallback={<span>Loading catalog...</span>}>
<button className='btn btn-light' onClick={toggleGlobalEdit}>Configure global parameters</button>
<h3>Nodes</h3>
<h4>Nodes</h4>
<ul>
{catalog?.categories.map((category) => <CatalogCategory key={category.name} {...category} />)}
</ul>
<h3>Examples</h3>
<h4>Examples</h4>
Workflow examples that can be loaded as a starting point.
<ul>
{Object.entries(catalog?.examples).map(([name, workflow]) => <Example key={name} name={name} workflow={workflow} />)}
Expand Down
4 changes: 2 additions & 2 deletions src/CatalogPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useRecoilState } from 'recoil'
import { catalogURLchoices } from './constants'
import { catalogURLState } from './store'

export const CatalogPicker = () => {
export const CatalogPicker = (): JSX.Element => {
const [url, setURL] = useRecoilState(catalogURLState)
// TODO clear workflow when switching catalogs
// TODO clear? workflow when switching catalogs
return (
<select value={url} onChange={(e) => setURL(e.target.value)}>
{catalogURLchoices.map(([k, v]) => <option key={v} value={v}>{k}</option>)}
Expand Down
32 changes: 32 additions & 0 deletions src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react'
import { ValidationError } from './validate'

interface State {
error: Error | null
}

export class ErrorBoundary extends React.Component<{}, State> {
static getDerivedStateFromError (error: Error): State {
return { error }
}

state: State = {
error: null
}

render (): React.ReactNode {
if (this.state.error !== null) {
if (this.state.error instanceof ValidationError) {
console.error(this.state.error.errors)
}
return (
<div>
<h1>Something went terribly wrong.</h1>
<span>{this.state.error.message}</span>
</div>
)
}

return this.props.children
}
}
32 changes: 30 additions & 2 deletions src/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import { toast } from 'react-toastify'
import { useWorkflow } from './store'

interface IProps {
name: string
workflow: string
}

export const Example = ({ name, workflow }: IProps) => {
export const Example = ({ name, workflow }: IProps): JSX.Element => {
const { loadWorkflowArchive } = useWorkflow()
return <li><button className='btn btn-light' onClick={async () => await loadWorkflowArchive(workflow)} title={workflow}>{name}</button></li>

async function onClick (): Promise<void> {
await toast.promise(
loadWorkflowArchive(workflow),
{
pending: 'Loading workfow ...',
success: 'Workflow loaded',
error: {
render ({ data }) {
console.error(data)
return 'Workflow archive failed to load. See DevTools (F12) console for errors.'
}
}
}
)
}

return (
<li>
<button
className='btn btn-light'
onClick={onClick}
title={workflow}
>
{name}
</button>
</li>
)
}
6 changes: 3 additions & 3 deletions src/FilesList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { saveAs } from 'file-saver'
import { useFiles } from './store'

export const FilesList = () => {
const { files } = useFiles()
export const FilesList = (): JSX.Element => {
const files = useFiles()

function downloadFile (filename: string) {
function downloadFile (filename: string): void {
saveAs(files[filename], filename)
}

Expand Down
5 changes: 3 additions & 2 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { Theme } from '@rjsf/bootstrap-4'
const registry = utils.getDefaultRegistry()
const DefaultFileWidget = registry.widgets.FileWidget;
(Theme as any).widgets.FileWidget = (props: WidgetProps) => {
const label = props.schema.title ?? props.label
return (
<div>
<label className='form-label'>{props.schema.title || props.label}
{(props.label || props.schema.title) && props.required ? '*' : null}
<label className='form-label'>{label}
{props.required ? '*' : null}
</label>
<DefaultFileWidget {...props} />
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/GlobalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { useCatalog, useFiles, useWorkflow } from './store'
import { internalizeDataUrls } from './dataurls'
import { Form } from './Form'

export const GlobalForm = () => {
export const GlobalForm = (): JSX.Element => {
const { global: globalSchemas } = useCatalog()
const { setParameters, global: parameters } = useWorkflow()
const { files } = useFiles()
const parametersWithDataUrls = internalizeDataUrls(parameters, files)
const { setGlobalParameters, global: parameters } = useWorkflow()
const files = useFiles()
const parametersWithDataUrls = internalizeDataUrls(parameters, files) // TODO move to hook, each time component is re-rendered this method will be called
const uiSchema = (globalSchemas.uiSchema != null) ? globalSchemas.uiSchema : {}
return (
<Form schema={globalSchemas.schema} uiSchema={uiSchema} formData={parametersWithDataUrls} onSubmit={({ formData }) => setParameters(formData)} />
<Form schema={globalSchemas.schema} uiSchema={uiSchema} formData={parametersWithDataUrls} onSubmit={({ formData }) => setGlobalParameters(formData)} />
)
}
2 changes: 1 addition & 1 deletion src/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCatalog } from './store'

export const Header = () => {
export const Header = (): JSX.Element => {
const { title } = useCatalog()
return (
<h1 style={{ height: '1em' }}>i-VRESSE workflow builder: {title}</h1>
Expand Down
30 changes: 30 additions & 0 deletions src/NodeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useFiles, useSelectedCatalogNode, useSelectedNode, useWorkflow } from './store'
import { internalizeDataUrls } from './dataurls'
import { Form } from './Form'

export const NodeForm = (): JSX.Element => {
// TODO move setParameters to useSelectedNode
const { setNodeParameters } = useWorkflow()
const files = useFiles()
const node = useSelectedNode()
const catalogNode = useSelectedCatalogNode()

if (node === undefined) {
return <div>No node selected</div>
}
if (catalogNode === undefined) {
return <div>Unable to find schema belonging to node</div>
}
const parametersWithDataUrls = internalizeDataUrls(node.parameters, files)

const uiSchema = (catalogNode?.uiSchema != null) ? catalogNode.uiSchema : {}
return (
<>
<h4>{catalogNode.label} ({node.id})</h4>
<div>
{catalogNode.description}
</div>
<Form schema={catalogNode.schema} uiSchema={uiSchema} formData={parametersWithDataUrls} onSubmit={({ formData }) => setNodeParameters(formData)} />
</>
)
}
23 changes: 23 additions & 0 deletions src/NodePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { GlobalForm } from './GlobalForm'
import { NodeForm } from './NodeForm'
import { useSelectNodeIndex, useWorkflow } from './store'

export const NodePanel = (): JSX.Element => {
const selectedNodeIndex = useSelectNodeIndex()
const { editingGlobal } = useWorkflow()
let form = <div>No node or global parameters selected for configuration.</div>
let legend = 'Node'
if (editingGlobal) {
form = <GlobalForm />
legend = 'Global parameters'
}
if (selectedNodeIndex !== -1) {
form = <NodeForm />
}
return (
<fieldset>
<legend>{legend}</legend>
{form}
</fieldset>
)
}
29 changes: 0 additions & 29 deletions src/StepForm.tsx

This file was deleted.

22 changes: 0 additions & 22 deletions src/StepPanel.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions src/TextPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const TextPanel = (): JSX.Element => {
await navigator.clipboard.writeText(code)
}

// TODO would be nice if text was editable and showed parameter description on hover and inline validation errors.
// TODO would be nice to be able to click in text to select step to edit.
return (
<div>
<HighlightedCode code={code} />
Expand Down
Loading