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

Improve creator UI #57

Merged
merged 1 commit into from
Aug 8, 2024
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
12 changes: 12 additions & 0 deletions src/Creator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.rc-header {
background-color: var(--pf-v5-global--BackgroundColor--dark-300);
color: var(--pf-v5-global--Color--light-100);
}

.rc-header a {
color: var(--pf-v5-global--link--Color--light);
}

.rc-header a:hover {
color: var(--pf-v5-global--link--Color--light--hover);
}
161 changes: 67 additions & 94 deletions src/Creator.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,40 @@
import React, { useMemo, useState } from 'react';
import YAML, { YAMLError } from 'yaml';
import YAML from 'yaml';
import {
Grid,
GridItem,
PageGroup,
PageSection,
Title,
} from '@patternfly/react-core';
import {
QuickStart,
QuickStartSpec,
QuickStartTask,
} from '@patternfly/quickstarts';
import { QuickStart, QuickStartSpec } 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>;
};
import './Creator.scss';
import { CreatorWizardStage } from './components/creator/schema';

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

function makeDemoQuickStart(
kind: ItemKind | null,
baseQuickStart: QuickStart,
taskContents: string[]
): [QuickStart, CreatorErrors] {
baseQuickStart: QuickStart
): QuickStart {
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,
},
return {
...baseQuickStart,
metadata: {
...baseQuickStart.metadata,
name: 'test-quickstart',
...(kindMeta?.extraMetadata ?? {}),
},
{ taskErrors },
];
};
}

const Creator = () => {
const CreatorInternal = ({ resetCreator }: { resetCreator: () => void }) => {
const [rawKind, setRawKind] = useState<ItemKind | null>(null);

const [rawQuickStart, setRawQuickStart] = useState<QuickStart>({
Expand All @@ -93,9 +52,11 @@ const Creator = () => {
rawKind !== null ? { id: rawKind, meta: metaForKind(rawKind) } : null;

const [bundles, setBundles] = useState<string[]>([]);
const [taskContents, setTaskContents] = useState<string[]>([]);
const [currentStage, setCurrentStage] = useState<CreatorWizardStage>({
type: 'card',
});

const [currentTask, setCurrentTask] = useState<number | null>(null);
const isDownloadStage = currentStage.type !== 'download';

const updateSpec = (
updater: (old: QuickStartSpec) => Partial<QuickStartSpec>
Expand Down Expand Up @@ -143,20 +104,14 @@ const Creator = () => {

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 quickStart = useMemo(
() => makeDemoQuickStart(rawKind, rawQuickStart),
[rawKind, rawQuickStart]
);

const files = useMemo(() => {
Expand All @@ -165,15 +120,18 @@ const Creator = () => {
.replaceAll(/\s/g, '-')
.replaceAll(/(^-+)|(-+$)/g, '');

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

delete adjustedQuickstart.spec['icon'];

return [
{
name: 'metadata.yaml',
Expand All @@ -192,49 +150,64 @@ const Creator = () => {
];
}, [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">
<PageSection className="rc-header">
<Title headingLevel="h1" size="2xl">
Add new learning resources
Add new learning resource
</Title>

<p>Description</p>
<p>
Add cards to the learning resources spaces within console.redhat.com.{' '}
<a
href="https://docs.google.com/presentation/d/1FiwBc_VuCxvobv80suXww0eKEs381MgR1WK6q7_DynY/edit#slide=id.g1b95fa54a9f_0_801"
target="_blank"
rel="noreferrer"
>
Learn more about Hybrid Cloud Console Learning Resources.
</a>
</p>
</PageSection>

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

<GridItem span={12} lg={6}>
<CreatorPreview
kindMeta={selectedKind?.meta ?? null}
quickStart={quickStart}
currentTask={currentTask}
/>
</GridItem>
{isDownloadStage ? (
<GridItem span={12} lg={6} className="pf-v5-u-pt-md-on-lg">
<CreatorPreview
kindMeta={selectedKind?.meta ?? null}
quickStart={quickStart}
currentStage={currentStage}
/>
</GridItem>
) : null}
</Grid>
</PageSection>
</PageGroup>
);
};

const Creator = () => {
const [resetCount, setResetCount] = useState(0n);

return (
<CreatorInternal
key={resetCount}
resetCreator={() => setResetCount((old) => old + 1n)}
/>
);
};

export default Creator;
62 changes: 62 additions & 0 deletions src/components/DdfNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
NumberInput,
} from '@patternfly/react-core';
import React, { useId } from 'react';
import {
UseFieldApiConfig,
useFieldApi,
} from '@data-driven-forms/react-form-renderer';
import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon';

const DdfNumberInput = (props: UseFieldApiConfig) => {
const { input, meta, ...rest } = useFieldApi(props);
const reactId = useId();
const effectiveId = rest.id || reactId;

const showInvalid = meta.touched && meta.invalid;

const focusProps = {
onFocus: input.onFocus,
onBlur: input.onBlur,
};

return (
<FormGroup
label={rest.label}
isRequired={rest.isRequired}
fieldId={effectiveId}
>
<NumberInput
id={effectiveId}
inputName={input.name}
value={input.value}
validated={showInvalid ? 'error' : 'default'}
min={rest.minValue}
max={rest.maxValue}
unit={rest.unit}
onPlus={() => input.onChange((input.value ?? 0) + 1)}
onMinus={() => input.onChange((input.value ?? 0) - 1)}
onChange={(e) => input.onChange(e.currentTarget.valueAsNumber)}
{...focusProps}
plusBtnProps={focusProps}
minusBtnProps={focusProps}
/>

{showInvalid ? (
<FormHelperText>
<HelperText>
<HelperTextItem variant={'error'} icon={<ExclamationCircleIcon />}>
{meta.error}
</HelperTextItem>
</HelperText>
</FormHelperText>
) : null}
</FormGroup>
);
};

export default DdfNumberInput;
26 changes: 26 additions & 0 deletions src/components/SimpleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { ButtonHTMLAttributes, ReactNode } from 'react';

const SimpleButton = ({
type = 'button',
children,
icon,
...rest
}: ButtonHTMLAttributes<HTMLButtonElement> & {
icon?: ReactNode;
}) => {
return (
<button
{...rest}
type={type}
className={`pf-v5-u-background-color-200 ${rest.className ?? ''}`}
style={{ border: 'none', ...(rest.style ?? {}) }}
>
<span className={icon !== undefined ? 'pf-v5-u-mr-sm' : ''}>
{children}
</span>
{icon}
</button>
);
};

export default SimpleButton;
14 changes: 14 additions & 0 deletions src/components/StringArrayInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.lr-string-array {
display: grid;
grid-template-columns: [label-start] auto [label-end value-start] 1fr [value-end remove-start] auto [remove-end];

.lr-string-array-label {
grid-column-start: label-start;
grid-column-end: label-end;
}

.lr-string-array-value {
grid-column-start: value-start;
grid-column-end: value-end;
}
}
Loading