Skip to content

Commit

Permalink
Add learning resources creator (#54)
Browse files Browse the repository at this point in the history
* Temporarily replace app with Creator component

I know this will have to be fixed at some point, but this seems like the easiest way to get started.

* Add basic type selection

* Add named types for item kinds

* Use Patternfly FormGroup for radio buttons

* Add form with bundle input

* Add title input

* Add description input

* Unify input prop types

* Use a loop for common item inputs

* Add placeholders

* Add URL input when type is documentation

* Add separate ItemFormContainer

* Add duration input when type is quickstart

* Add required indicators to field groups

* Move type field to own component

* Add labeled FormGroup around type input

* Add labels to DurationInput

* Use InputProps for all applicable components

* Use grid to divide screen into columns

* Make step headers fancy

* Remove incorrect section around title

* Clamp duration input at 0

* Show tile preview with QuickStartCatalog

* Move QuickStartTile usage to separate component

* Move lr-c-quickstart_tile into WrappedQuickStartTile

* Use WrappedQuickStartTile in Creator

* Move label colors to itemKindMeta

* Make creator look minimally usable

* Make page header full row

* Add "Live card preview" header

* Use margin-inline-end for rc-step-index

* Define ItemKind using itemKindMeta

* Add new item types

* Define fields in item metadata

* Use useMemo for QuickStart

* Half-functioning quickstart tester

* Use QuickStartDrawer directly

* Add skeleton of wizard

* Move duration field into wizard

* Properly set input IDs for labels

* Remove now-unused components

* Allow selecting multiple bundles

* Prevent attempt to read quickstart tasks when they don't exist

* Require progressing through wizard in order

* Use cast instead of lambda for inputItemDesc

* Remove uses of useId

* Add task overview page

* Separate Creator code into several files

* Use hidden steps instead of setting key on Wizard

* Allow adding more tasks

* Add dependency on Patternfly code-editor

* Add basic panel editing

* Allow removing tasks

* Cleanup task array modification

* Remove ItemFormElement

* Add label to remove task buttons

* Move string array input to component

* Allow modifying quickstart prerequisites

* Allow editing task work-check values

* Fix setting value of CodeEditor

* Add yaml dependency

* Add rendering of files and basic display

* Improve quickstart name generation

* Remove log statement

* Don't include icon in quickstart file

* Add ability to download files

* Restore original app homepage

* Allow switching between viewer and creator

* Use a flag to enable creator

* Accept YAML for step config rather than form fields

* Include per-type metadata in export

* Use footer config from chrome in creator

* Use react-router for creator/viewer parts

* Update quickstarts extension

* Ensure quickstart is not focused on

* Fix task editor description

* Don't use useMemo for selectedType

* Remove unnecessary string template

* Remove unnecessary children node

* Remove memo of getAvailableBundles()

* Normalize SelectMultiTypeahead hooks

* Remove useMemo import from CreatorInputs

* Import icons from dynamic rather than esm

* Use undefined to indicate no results in SelectMultiTypeahead

* Remove extraneous lambda

* Replace only whitespace in quickstart name

* Don't useMemo another selectedType

* Use whitespace regex in SelectMultiTypeahead

* Use a single Form element

* Prevent form submission

* Fix nested FormGroups

* Show errors when parsing task YAML

* Fix handling of no search results in SelectMultiTypeahead

* Use quickstart state directly

* Fix quickstart metadata

* Ensure Wizard always take up at least all height

* Wrap task error display

* Use useMemo in SelectMultiTypeahead

* Use unleash directly in App

* Make Creator its own independent app

* Move creator preview to separate file

* Always show quickstart drawer

* Cleanup leftover context parts in Creator

* Sync preview task to wizard step

* Always use a string key in SelectMultiTypeahead

* Remove unused option id in SelectMultiTypeahead

* Use an actually-unique ID in SelectMultiTypeahead

* Add Data Driven Forms dependencies

* Remove commented-out code

* Add isItemKind type guard

* Use basic data-driven form with overview page

* Add ALL_ITEM_KINDS

* Match type and details steps of original form

* Get tile preview working

* Add basic panel editor

* Revert to uncontrolled panel preview

* Add field to show task YAML errors

* Ensure that task errors wrap

* Add step to download files

* Revert "Revert to uncontrolled panel preview"

This reverts commit 7c6fa87.

* Update task preview as moving through wizard

* Remove missing content placeholder

* Remove unused code

* Fix "explicit use of any" error on FormValue

* Document the PropUpdater hack

* Replace ugly hack with much cleaner hack

* Remove logging

* Don't use FormTemplate

* Fix extra array around tags

* Use WizardProps type for wizard schema

* Update Patternfly and tsc-transform-inputs in order to get automatic module finding

* Use custom buttons for wizard footer

* Remove SelectMultiTypeahead

* Update schema comments

* Remove unused CSS

* Remove unnecessary Viewer

* Rename App to Viewer

* Encapsulate uses of itemKindMeta

* Standardize on "kind" rather than "type"

* Remove broken import

* Further kind fixes

* Fix button handling

* Move CreatorPreview CSS to proper component

* Remove CatalogSection CSS import

* Sync react-code-editor
  • Loading branch information
randomnetcat authored Jul 11, 2024
1 parent 8dd76b7 commit 360ef47
Show file tree
Hide file tree
Showing 16 changed files with 1,593 additions and 131 deletions.
1 change: 1 addition & 0 deletions fec.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
__dirname,
'./src/components/GlobalLearningResourcesPage/GlobalLearningResourcesPage'
),
'./Creator': path.resolve(__dirname, './src/Creator.tsx'),
},
exclude: ['react-router-dom'],
shared: [
Expand Down
468 changes: 413 additions & 55 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
"postinstall": "ts-patch install"
},
"dependencies": {
"@patternfly/quickstarts": "5.2.0-prerelease.3",
"@patternfly/react-core": "^5.1.2",
"@data-driven-forms/pf4-component-mapper": "^3.23.0",
"@data-driven-forms/react-form-renderer": "^3.23.0",
"@patternfly/quickstarts": "^5.4.0-prerelease.1",
"@patternfly/react-code-editor": "^5.3.4",
"@patternfly/react-core": "^5.3.4",
"@patternfly/react-table": "^5.1.1",
"@redhat-cloud-services/frontend-components": "^4.2.9",
"@redhat-cloud-services/frontend-components-notifications": "^4.0.4",
Expand All @@ -31,12 +34,13 @@
"axios": "^1.6.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "^6.22.3"
"react-router-dom": "^6.22.3",
"yaml": "^2.4.5"
},
"devDependencies": {
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.3",
"@redhat-cloud-services/frontend-components-config": "^6.0.5",
"@redhat-cloud-services/tsc-transform-imports": "^1.0.4",
"@redhat-cloud-services/tsc-transform-imports": "^1.0.15",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.1.2",
"@types/react": "18.2.46",
Expand Down
4 changes: 2 additions & 2 deletions src/AppEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { App } from './App';
import { Viewer } from './Viewer';

const AppEntry = (props: { bundle: string }) => <App {...props} />;
const AppEntry = (props: { bundle: string }) => <Viewer {...props} />;

export default AppEntry;
240 changes: 240 additions & 0 deletions src/Creator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import React, { useMemo, useState } from 'react';
import YAML, { YAMLError } from 'yaml';
import {
Grid,
GridItem,
PageGroup,
PageSection,
Title,
} from '@patternfly/react-core';
import {
QuickStart,
QuickStartSpec,
QuickStartTask,
} from '@patternfly/quickstarts';
import CreatorWizard, { EMPTY_TASK } from './components/creator/CreatorWizard';
import { ItemKind, metaForKind } from './components/creator/meta';
import CreatorPreview from './components/creator/CreatorPreview';

export type CreatorErrors = {
taskErrors: Map<number, string>;
};

const BASE_METADATA = {
name: 'test-quickstart',
};

function makeDemoQuickStart(
kind: ItemKind | null,
baseQuickStart: QuickStart,
taskContents: string[]
): [QuickStart, CreatorErrors] {
const kindMeta = kind !== null ? metaForKind(kind) : null;

const [tasks, taskErrors] = (() => {
if (kindMeta?.hasTasks !== true) return [undefined, new Map()];

const out: QuickStartTask[] = [];
const errors: CreatorErrors['taskErrors'] = new Map();

if (baseQuickStart.spec.tasks !== undefined) {
for (let index = 0; index < baseQuickStart.spec.tasks.length; ++index) {
const task = baseQuickStart.spec.tasks[index];

try {
out.push({
...YAML.parse(taskContents[index]),
title: task.title,
});
} catch (e) {
if (!(e instanceof YAMLError)) throw e;

out.push({ ...EMPTY_TASK, title: task.title });
errors.set(index, e.message);
}
}
}

return [out, errors];
})();

return [
{
...baseQuickStart,
metadata: {
...baseQuickStart.metadata,
name: 'test-quickstart',
...(kindMeta?.extraMetadata ?? {}),
},
spec: {
...baseQuickStart.spec,
tasks: tasks,
},
},
{ taskErrors },
];
}

const Creator = () => {
const [rawKind, setRawKind] = useState<ItemKind | null>(null);

const [rawQuickStart, setRawQuickStart] = useState<QuickStart>({
metadata: {
name: 'test-quickstart',
},
spec: {
displayName: '',
icon: null,
description: '',
},
});

const selectedKind =
rawKind !== null ? { id: rawKind, meta: metaForKind(rawKind) } : null;

const [bundles, setBundles] = useState<string[]>([]);
const [taskContents, setTaskContents] = useState<string[]>([]);

const [currentTask, setCurrentTask] = useState<number | null>(null);

const updateSpec = (
updater: (old: QuickStartSpec) => Partial<QuickStartSpec>
) => {
setRawQuickStart((old) => ({
...old,
spec: {
...old.spec,
...updater(old.spec),
},
}));
};

const setKind = (newKind: ItemKind | null) => {
if (newKind !== null) {
const meta = metaForKind(newKind);

setRawQuickStart((old) => {
const updates: Partial<QuickStart> = {};

updates.spec = { ...old.spec };

updates.spec.type = {
text: meta.displayName,
color: meta.tagColor,
};

if (
meta.hasTasks &&
(old.spec.tasks === undefined || old.spec.tasks.length === 0)
) {
updates.spec.tasks = [EMPTY_TASK];
}

if (!meta.hasTasks) {
updates.spec.tasks = undefined;
updates.spec.introduction = undefined;
updates.spec.prerequisites = [];
}

if (!meta.fields.url) updates.spec.link = undefined;
if (!meta.fields.duration) updates.spec.durationMinutes = undefined;

updates.metadata = { ...BASE_METADATA, ...meta.extraMetadata };

return { ...old, ...updates };
});

if (meta.hasTasks) {
setTaskContents((old) => (old.length === 0 ? [''] : old));
} else if (!meta.hasTasks) {
setTaskContents([]);
}
}

setRawKind(newKind);
};

const [quickStart, errors] = useMemo(
() => makeDemoQuickStart(rawKind, rawQuickStart, taskContents),
[rawKind, rawQuickStart, taskContents]
);

const files = useMemo(() => {
const effectiveName = quickStart.spec.displayName
.toLowerCase()
.replaceAll(/\s/g, '-')
.replaceAll(/(^-+)|(-+$)/g, '');

const adjustedQuickstart = { ...quickStart };
adjustedQuickstart.spec = { ...adjustedQuickstart.spec };
adjustedQuickstart.metadata = {
...adjustedQuickstart.metadata,
name: effectiveName,
};

delete adjustedQuickstart.spec['icon'];

return [
{
name: 'metadata.yaml',
content: YAML.stringify({
kind: 'QuickStarts',
name: effectiveName,
tags: bundles
.toSorted()
.map((bundle) => ({ kind: 'bundle', value: bundle })),
}),
},
{
name: `${effectiveName}.yaml`,
content: YAML.stringify(adjustedQuickstart),
},
];
}, [quickStart, bundles]);

if ((quickStart.spec.tasks?.length ?? 0) != taskContents.length) {
throw new Error(
`Mismatch between quickstart tasks and task contents: ${quickStart.spec.tasks?.length} vs ${taskContents.length}`
);
}

return (
<PageGroup>
<PageSection variant="darker">
<Title headingLevel="h1" size="2xl">
Add new learning resources
</Title>

<p>Description</p>
</PageSection>

<PageSection isFilled>
<Grid hasGutter className="pf-v5-u-h-100 pf-v5-u-w-100">
<GridItem span={12} lg={6}>
<CreatorWizard
onChangeKind={setKind}
onChangeQuickStartSpec={(spec) => {
updateSpec(() => spec);
}}
onChangeBundles={setBundles}
onChangeTaskContents={setTaskContents}
onChangeCurrentTask={setCurrentTask}
errors={errors}
files={files}
/>
</GridItem>

<GridItem span={12} lg={6}>
<CreatorPreview
kindMeta={selectedKind?.meta ?? null}
quickStart={quickStart}
currentTask={currentTask}
/>
</GridItem>
</Grid>
</PageSection>
</PageGroup>
);
};

export default Creator;
File renamed without changes.
4 changes: 2 additions & 2 deletions src/App.tsx → src/Viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import './App.scss';
import './Viewer.scss';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import {
LoadingBox,
Expand All @@ -26,7 +26,7 @@ import { BookmarkIcon, OutlinedBookmarkIcon } from '@patternfly/react-icons';
import { useFlag } from '@unleash/proxy-client-react';
import useQuickStarts from './hooks/useQuickStarts';

export const App = ({ bundle }: { bundle: string }) => {
export const Viewer = ({ bundle }: { bundle: string }) => {
const chrome = useChrome();
const { activeQuickStartID, allQuickStartStates, setFilter, loading } =
React.useContext<QuickStartContextValues>(QuickStartContext);
Expand Down
15 changes: 0 additions & 15 deletions src/components/CatalogSection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,3 @@
margin-bottom: var(--pf-global--spacer--md);
}
}

.lr-c-quickstart_tile {
.pf-v5-c-card__header {
height: 0;
}
.pf-v5-c-title {
flex: 1;
}
>div {
height: 100%;
>a {
color: var(--pf-v5-global--palette--black-1000);
}
}
}
Loading

0 comments on commit 360ef47

Please sign in to comment.