Skip to content

Commit

Permalink
Quilt config editor (#3121)
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus authored Oct 7, 2022
1 parent 0b6543c commit 13e22c0
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 54 deletions.
31 changes: 28 additions & 3 deletions catalog/app/components/FileEditor/FileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { S3HandleBase } from 'utils/s3paths'

import Skeleton from './Skeleton'
import TextEditor from './TextEditor'
import QuiltConfigEditor from './QuiltConfigEditor'
import { detect, loadMode, useWriteData } from './loader'
import { EditorInputType } from './types'

Expand Down Expand Up @@ -110,10 +111,23 @@ function EditorSuspended({
onChange,
type,
}: EditorProps) {
loadMode(type.brace || 'plain_text') // TODO: loaders#typeText.brace
if (type.brace !== '__quiltConfig') {
loadMode(type.brace || 'plain_text') // TODO: loaders#typeText.brace
}

const data = PreviewUtils.useObjectGetter(handle, { noAutoFetch: empty })
if (empty) return <TextEditor error={error} type={type} value="" onChange={onChange} />
if (empty)
return type.brace === '__quiltConfig' ? (
<QuiltConfigEditor
handle={handle}
disabled={disabled}
error={error}
onChange={onChange}
initialValue=""
/>
) : (
<TextEditor error={error} type={type} value="" onChange={onChange} />
)
return data.case({
_: () => <Skeleton />,
Err: (
Expand All @@ -124,8 +138,19 @@ function EditorSuspended({
<PreviewDisplay data={AsyncResult.Err(err)} />
</div>
),
Ok: (response: $TSFixMe) => {
Ok: (response: { Body: Buffer }) => {
const value = response.Body.toString('utf-8')
if (type.brace === '__quiltConfig') {
return (
<QuiltConfigEditor
handle={handle}
disabled={disabled}
error={error}
onChange={onChange}
initialValue={value}
/>
)
}
return (
<TextEditor
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react'

import bucketPreferencesSchema from 'schemas/bucketConfig.yml.json'

import type { JsonSchema } from 'utils/json-schema'

interface BucketPreferencesProps {
children: (props: { schema: JsonSchema }) => React.ReactElement
}

export default function BucketPreferences({ children }: BucketPreferencesProps) {
return children({ schema: bucketPreferencesSchema })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ErrorObject } from 'ajv'
import * as React from 'react'
import * as M from '@material-ui/core'

import JsonEditor from 'components/JsonEditor'
import JsonValidationErrors from 'components/JsonValidationErrors'
import { JsonSchema, makeSchemaValidator } from 'utils/json-schema'
import * as YAML from 'utils/yaml'

const useStyles = M.makeStyles((t) => ({
root: {
display: 'flex',
flexDirection: 'column',
width: '100%',
},
errors: {
marginTop: t.spacing(1),
},
}))

export interface QuiltConfigEditorProps {
disabled?: boolean
onChange: (value: string) => void
initialValue?: string
error: Error | null
}

export default function QuiltConfigEditorSuspended({
disabled,
onChange,
initialValue,
error,
schema,
}: QuiltConfigEditorProps & { schema?: JsonSchema }) {
const classes = useStyles()
const validate = React.useMemo(() => makeSchemaValidator(schema), [schema])
const [errors, setErrors] = React.useState<(Error | ErrorObject)[]>(
error ? [error] : [],
)
const [value, setValue] = React.useState(YAML.parse(initialValue))
const handleChange = React.useCallback(
(json) => {
setErrors(validate(json))
setValue(json)
onChange(YAML.stringify(json))
},
[onChange, validate],
)
return (
<div className={classes.root}>
<JsonEditor
disabled={disabled}
errors={errors}
multiColumned
onChange={handleChange}
value={value}
schema={schema}
/>
<JsonValidationErrors className={classes.errors} error={errors} />
</div>
)
}
13 changes: 13 additions & 0 deletions catalog/app/components/FileEditor/QuiltConfigEditor/Workflows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react'

import workflowsBaseSchema from 'schemas/workflows-config-1.1.0.json'

import type { JsonSchema } from 'utils/json-schema'

interface WorkflowsProps {
children: (props: { schema: JsonSchema }) => React.ReactElement
}

export default function Workflows({ children }: WorkflowsProps) {
return children({ schema: workflowsBaseSchema })
}
44 changes: 44 additions & 0 deletions catalog/app/components/FileEditor/QuiltConfigEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as React from 'react'

import * as quiltConfigs from 'constants/quiltConfigs'
import type { S3HandleBase } from 'utils/s3paths'

import Skeleton from '../Skeleton'

import type { QuiltConfigEditorProps } from './QuiltConfigEditor'

const BucketPreferences = React.lazy(() => import('./BucketPreferences'))
const Workflows = React.lazy(() => import('./Workflows'))

function DummySchemaFetcher({
children,
}: {
children: (props: { schema: undefined }) => React.ReactElement
}) {
return children({ schema: undefined })
}

const QuiltConfigEditorSuspended = React.lazy(() => import('./QuiltConfigEditor'))

function getSchemaFetcher(handle: S3HandleBase) {
if (
quiltConfigs.bucketPreferences.some((quiltConfig) => quiltConfig.includes(handle.key))
)
return BucketPreferences
if (quiltConfigs.workflows.includes(handle.key)) return Workflows
return DummySchemaFetcher
}

export default ({
handle,
...props
}: QuiltConfigEditorProps & { handle: S3HandleBase }) => {
const SchemaFetcher = getSchemaFetcher(handle)
return (
<React.Suspense fallback={<Skeleton />}>
<SchemaFetcher>
{({ schema }) => <QuiltConfigEditorSuspended {...props} schema={schema} />}
</SchemaFetcher>
</React.Suspense>
)
}
8 changes: 8 additions & 0 deletions catalog/app/components/FileEditor/loader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as R from 'ramda'
import * as React from 'react'

import * as quiltConfigs from 'constants/quiltConfigs'
import { detect as isMarkdown } from 'components/Preview/loaders/Markdown'
import * as PreviewUtils from 'components/Preview/loaders/utils'
import * as AWS from 'utils/AWS'
Expand All @@ -20,6 +21,12 @@ export const loadMode = (mode: Mode) => {
throw cache[mode]
}

const isQuiltConfig = (path: string) =>
quiltConfigs.all.some((quiltConfig) => quiltConfig.includes(path))
const typeQuiltConfig: EditorInputType = {
brace: '__quiltConfig',
}

const isCsv = PreviewUtils.extIn(['.csv', '.tsv', '.tab'])
const typeCsv: EditorInputType = {
brace: 'less',
Expand Down Expand Up @@ -51,6 +58,7 @@ const typeNone: EditorInputType = {
export const detect: (path: string) => EditorInputType = R.pipe(
PreviewUtils.stripCompression,
R.cond([
[isQuiltConfig, R.always(typeQuiltConfig)],
[isCsv, R.always(typeCsv)],
[isJson, R.always(typeJson)],
[isMarkdown, R.always(typeMarkdown)],
Expand Down
2 changes: 1 addition & 1 deletion catalog/app/components/FileEditor/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Mode = 'less' | 'json' | 'markdown' | 'plain_text' | 'yaml'
export type Mode = '__quiltConfig' | 'less' | 'json' | 'markdown' | 'plain_text' | 'yaml'

export interface EditorInputType {
brace: Mode | null
Expand Down
73 changes: 52 additions & 21 deletions catalog/app/components/JsonEditor/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,71 @@ import Column from './Column'
import State from './State'
import { JsonValue, RowData, ValidationErrors } from './constants'

interface EmptyStateCaseProps {
children: React.ReactNode
className: string
title: string
video: string
}

function EmptyStateCase({ children, className, title, video }: EmptyStateCaseProps) {
const videoRef = React.useRef<HTMLVideoElement | null>(null)
const togglePlayback = React.useCallback(() => {
const el = videoRef?.current
if (el?.paused) {
el?.play()
} else {
el?.pause()
}
}, [videoRef])
return (
<M.Card className={className}>
<M.CardContent>
<M.Typography variant="h5">{title}</M.Typography>
<M.Typography variant="body1">{children}</M.Typography>
<video
ref={videoRef}
src={video}
width="100%"
autoPlay
loop
onClick={togglePlayback}
/>
</M.CardContent>
</M.Card>
)
}

interface EmptyStateProps {
className: string
noValue: boolean
notExpanded: boolean
}

function EmptyState({ className, noValue, notExpanded }: EmptyStateProps) {
if (noValue) {
if (noValue && notExpanded) {
return (
<M.Card className={className}>
<M.CardContent>
<M.Typography variant="h5">JSON editor is empty</M.Typography>
<M.Typography variant="body1">
Start filling empty rows as in Excel. You can enter values by hand. Type{' '}
<Code>{`{}`}</Code> to create an object, or <Code>{`[]`}</Code> to create an
array, and then traverse it to enter properties.
</M.Typography>
<video src={illustrationEnterValues} width="100%" autoPlay loop />
</M.CardContent>
</M.Card>
<EmptyStateCase
className={className}
title="JSON editor is empty"
video={illustrationEnterValues}
>
Start filling empty rows as in Excel. You can enter values by hand. Type{' '}
<Code>{`{}`}</Code> to create an object, or <Code>{`[]`}</Code> to create an
array, and then traverse it to enter properties.
</EmptyStateCase>
)
}

if (notExpanded) {
return (
<M.Card className={className}>
<M.CardContent>
<M.Typography variant="h5">There is more data here</M.Typography>
<M.Typography variant="body1">Try to expand object values</M.Typography>
<video src={illustrationObjectExpand} width="100%" autoPlay loop />
</M.CardContent>
</M.Card>
<EmptyStateCase
className={className}
title="There is more data here"
video={illustrationObjectExpand}
>
Try to expand object values
</EmptyStateCase>
)
}

Expand Down Expand Up @@ -114,7 +147,6 @@ const useStyles = M.makeStyles<any, { multiColumned: boolean }>((t) => ({
height: ({ multiColumned }) => (multiColumned ? '100%' : 'auto'),
overflow: 'auto',
position: 'relative',
zIndex: 20,
},
column: {
height: ({ multiColumned }) => (multiColumned ? '100%' : 'auto'),
Expand All @@ -126,7 +158,6 @@ const useStyles = M.makeStyles<any, { multiColumned: boolean }>((t) => ({
right: 0,
top: 0,
width: t.spacing(60),
zIndex: 10,
},
}))

Expand Down
15 changes: 15 additions & 0 deletions catalog/app/constants/quiltConfigs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const bucketPreferences = [
'.quilt/catalog/config.yaml',
'.quilt/catalog/config.yml',
]

export const esQueries = '.quilt/queries/config.yaml'

// TODO: enable this when backend is ready
// const workflows = [
// '.quilt/workflows/config.yaml',
// '.quilt/workflows/config.yml',
// ]
export const workflows = '.quilt/workflows/config.yml'

export const all = [...bucketPreferences, esQueries, workflows]
13 changes: 8 additions & 5 deletions catalog/app/containers/Bucket/Queries/requests/queriesConfig.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import * as R from 'ramda'

import * as quiltConfigs from 'constants/quiltConfigs'
import * as errors from 'containers/Bucket/errors'
import * as requests from 'containers/Bucket/requests'
import * as AWS from 'utils/AWS'
import { useData } from 'utils/Data'
import yaml from 'utils/yaml'
import * as YAML from 'utils/yaml'

import { AsyncData } from './requests'

const QUERIES_CONFIG_PATH = '.quilt/queries/config.yaml'

// TODO: rename to requests.es.Query
export interface Query {
description?: string
Expand Down Expand Up @@ -48,9 +47,13 @@ export const queriesConfig = async ({
bucket,
}: QueriesConfigArgs): Promise<Query[] | null> => {
try {
const response = await requests.fetchFile({ s3, bucket, path: QUERIES_CONFIG_PATH })
const response = await requests.fetchFile({
s3,
bucket,
path: quiltConfigs.esQueries,
})
// TODO: validate config with JSON Schema
return parseQueriesList(yaml(response.Body.toString('utf-8')))
return parseQueriesList(YAML.parse(response.Body.toString('utf-8')))
} catch (e) {
if (e instanceof errors.FileNotFound || e instanceof errors.VersionNotFound) return []

Expand Down
3 changes: 2 additions & 1 deletion catalog/app/containers/Bucket/requests/requestsUntyped.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as R from 'ramda'
import quiltSummarizeSchema from 'schemas/quilt_summarize.json'

import { SUPPORTED_EXTENSIONS as IMG_EXTS } from 'components/Thumbnail'
import * as quiltConfigs from 'constants/quiltConfigs'
import * as Resource from 'utils/Resource'
import { makeSchemaValidator } from 'utils/json-schema'
import mkSearch from 'utils/mkSearch'
Expand Down Expand Up @@ -252,7 +253,7 @@ export const metadataSchema = async ({ s3, schemaUrl }) => {
return JSON.parse(response.Body.toString('utf-8'))
}

export const WORKFLOWS_CONFIG_PATH = '.quilt/workflows/config.yml'
export const WORKFLOWS_CONFIG_PATH = quiltConfigs.workflows
// TODO: enable this when backend is ready
// const WORKFLOWS_CONFIG_PATH = [
// '.quilt/workflows/config.yaml',
Expand Down
Loading

0 comments on commit 13e22c0

Please sign in to comment.