Skip to content
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
3 changes: 3 additions & 0 deletions SCHEMA_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ exclusive with `name`.

## parent

The whole section is optional: a devfile that does not inherit from a parent is valid, so an empty
parent step generates no `parent` key and raises neither an error nor a warning.

Source: `uri`, `id` (with `registryUrl` and `version`) or `kubernetes` (`name`, `namespace`).

Overrides: `components`, `commands`, `projects`, `starterProjects`, `dependentProjects`,
Expand Down
47 changes: 40 additions & 7 deletions src/components/steps/ParentStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getComponentType,
getParentSourceType
} from '../../utils/factories'
import Badge from '../common/Badge'
import CommandEditor from '../editors/CommandEditor'
import ComponentEditor from '../editors/ComponentEditor'
import ProjectEditor from '../editors/ProjectEditor'
Expand All @@ -26,6 +27,18 @@ import WizardNavigation from '../wizard/WizardNavigation'

const ISSUE_PREFIXES = ['Parent']

const DOCS_URL = 'https://devfile.io/docs/2.3.0/referring-to-a-parent-devfile'
const SCHEMA_DOCS_URL = 'https://devfile.io/docs/2.3.0/devfile-schema'

/** Link to the devfile.io page describing the section edited here. */
function DocsLink({ href, children }) {
return (
<a href={href} target="_blank" rel="noreferrer" className="text-blue-600 hover:underline">
{children}
</a>
)
}

const PROJECT_LISTS = [
{ kind: 'projects', title: 'Project overrides', prefix: 'parent-project', addButtonText: 'Add Project Override' },
{
Expand Down Expand Up @@ -68,14 +81,27 @@ export default function ParentStep() {
if (!parent) {
return (
<div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Parent</h2>
<p className="text-gray-600 mb-6">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-2xl font-bold text-gray-900">Parent</h2>
<Badge variant="default">Optional</Badge>
</div>
<p className="text-gray-600 mb-2">
A devfile can inherit from a parent devfile and override parts of it. Everything defined here is merged on
top of the parent.
top of the parent. Most devfiles do not use a parent — skipping this step is perfectly normal and the{' '}
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">parent</code> section is simply left out of the
generated devfile.
</p>
<p className="text-sm text-gray-500 mb-6">
See <DocsLink href={DOCS_URL}>Referring to a parent devfile</DocsLink> and the{' '}
<DocsLink href={SCHEMA_DOCS_URL}>devfile 2.3.0 schema</DocsLink>.
</p>

<div className="text-center py-10 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<p className="text-gray-500 mb-4">This devfile does not inherit from a parent.</p>
<p className="text-gray-500 mb-1">This devfile does not inherit from a parent.</p>
<p className="text-gray-400 text-sm mb-4">
Leave it this way unless you are extending a stack from a registry, a devfile URI or a
DevWorkspaceTemplate.
</p>
<Button variant="outline" onClick={() => setParent(createParent())}>
Add Parent
</Button>
Expand Down Expand Up @@ -105,14 +131,21 @@ export default function ParentStep() {
return (
<div>
<div className="flex justify-between items-start mb-2">
<h2 className="text-2xl font-bold text-gray-900">Parent</h2>
<div className="flex items-center gap-2">
<h2 className="text-2xl font-bold text-gray-900">Parent</h2>
<Badge variant="default">Optional</Badge>
</div>
<Button variant="danger" onClick={() => setParent(null)} className="text-xs px-2 py-1">
Remove Parent
</Button>
</div>
<p className="text-gray-600 mb-6">
<p className="text-gray-600 mb-2">
The parent devfile this one inherits from. Overrides below are merged on top of the parent content, so no
field is mandatory in them.
field is mandatory in them. Remove the parent to go back to a devfile without inheritance.
</p>
<p className="text-sm text-gray-500 mb-6">
See <DocsLink href={DOCS_URL}>Referring to a parent devfile</DocsLink> and the{' '}
<DocsLink href={SCHEMA_DOCS_URL}>devfile 2.3.0 schema</DocsLink>.
</p>

{/* A parent that was just added has no source yet, which the required
Expand Down
7 changes: 4 additions & 3 deletions src/components/steps/ReviewStep.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ function countBy(items, resolve) {
return [...counts.entries()].map(([key, count]) => `${count} ${key}`).join(', ')
}

function SummaryCard({ title, stepId, configured, onEdit, children }) {
function SummaryCard({ title, stepId, configured, notConfiguredLabel = 'Not configured', onEdit, children }) {
return (
<Card className="mb-4">
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
<Badge variant={configured ? 'success' : 'default'} className="mt-1">
{configured ? 'Configured' : 'Not configured'}
{configured ? 'Configured' : notConfiguredLabel}
</Badge>
</div>
<Button variant="outline" onClick={() => onEdit(STEP_INDEX[stepId])} className="text-xs px-2 py-1">
Expand Down Expand Up @@ -145,6 +145,7 @@ export default function ReviewStep() {
title="Parent"
stepId="parent"
configured={devfile.parent !== undefined}
notConfiguredLabel="Not used (optional)"
onEdit={handleEdit}
>
{devfile.parent ? (
Expand All @@ -153,7 +154,7 @@ export default function ReviewStep() {
{devfile.parent.uri || devfile.parent.id || devfile.parent.kubernetes?.name}
</p>
) : (
<p className="text-gray-500">No parent devfile</p>
<p className="text-gray-500">No parent devfile, the section is left out</p>
)}
</SummaryCard>

Expand Down
7 changes: 3 additions & 4 deletions src/utils/devfileGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,12 +1280,11 @@ export function validateDevfile(data) {
errors.push(`Schema version "${devfile.schemaVersion}" is not a valid devfile schema version`)
}

// Inheriting from a parent is optional, so a devfile without a parent section
// is never reported. A parent that holds overrides but no source still builds
// a section, which `validateParentSection` rejects.
const options = { hasParent: devfile.parent !== undefined }

if (data.parent && devfile.parent === undefined) {
warnings.push('Parent: no parent source configured, the section is left out of the devfile')
}

validateMetadataSection(devfile.metadata, errors, warnings)
validateParentSection(devfile.parent, errors, warnings)
for (const [kind, label] of PROJECT_LABELS) {
Expand Down
21 changes: 21 additions & 0 deletions tests/e2e/full-coverage.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,27 @@ test.describe('full schema coverage through the UI', () => {
await expect(page.getByRole('button', { name: 'Add Parent' })).toBeVisible()
expect((await readYaml(page)).parent).toBeUndefined()
})

test('the parent step is optional and skipping it raises nothing', async ({ page }) => {
await page.goto('/')
await page.fill('input[name="name"]', 'sample')

await goToStep(page, 'Parent')
await expect(page.getByText('Optional', { exact: true })).toBeVisible()
await expect(page.getByRole('link', { name: 'Referring to a parent devfile' })).toHaveAttribute(
'href',
'https://devfile.io/docs/2.3.0/referring-to-a-parent-devfile'
)

// An empty parent section is equivalent to no parent at all.
await page.getByRole('button', { name: 'Add Parent' }).click()

await goToStep(page, 'Review & Download')
expect((await readYaml(page)).parent).toBeUndefined()
await expect(page.getByText('Not used (optional)')).toBeVisible()
await expect(page.getByTestId('review-warnings')).toHaveCount(0)
await expect(page.getByText('Valid devfile')).toBeVisible()
})
})

test.describe('validation', () => {
Expand Down
16 changes: 13 additions & 3 deletions tests/unit/devfile-validation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,22 @@ test.describe('events', () => {
})

test.describe('parent', () => {
test('exactly one parent source is required', () => {
test('a devfile without a parent is not reported', () => {
expect(warningsOf(state()).filter((message) => message.startsWith('Parent'))).toEqual([])

// An empty parent section produces no devfile content, which is the same
// as not inheriting at all.
const data = state({ parent: { ...createParent(), uri: '' } })
expect(warningsOf(data).filter((message) => message.startsWith('Parent'))).toEqual([])
expect(errorsOf(data).filter((message) => message.startsWith('Parent'))).toEqual([])
})

expectMessage(warningsOf(data), 'no parent source configured')
test('exactly one parent source is required', () => {
const data = state({ parent: { ...createParent(), uri: 'https://example.com/devfile.yaml', id: 'nodejs' } })
expectMessage(errorsOf(data), 'exactly one of uri, id or kubernetes must be set')

data.parent = { ...createParent(), uri: 'https://example.com/devfile.yaml', id: 'nodejs' }
// Overrides without a source would be dropped silently.
data.parent = { ...createParent(), uri: '', components: [container('tools')] }
expectMessage(errorsOf(data), 'exactly one of uri, id or kubernetes must be set')
})

Expand Down
Loading