Skip to content
Open
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
6 changes: 6 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@
"Base domain resource group name": "Base domain resource group name",
"Basic information": "Basic information",
"Binding name": "Binding name",
"Bold": "Bold",
"Branch": "Branch",
"Bucket": "Bucket",
"Bucket name": "Bucket name",
Expand Down Expand Up @@ -1373,6 +1374,7 @@
"Edit application set - Pull model": "Edit application set - Pull model",
"Edit application set - push model": "Edit application set - push model",
"Edit channel": "Edit channel",
"Edit cluster description": "Edit cluster description",
"Edit credential": "Edit credential",
"Edit MultiClusterHub": "Edit MultiClusterHub",
"Edit placement": "Edit placement",
Expand Down Expand Up @@ -1427,6 +1429,7 @@
"Enter a name for this search query": "Enter a name for this search query",
"Enter a value": "Enter a value",
"Enter bucket information": "Enter bucket information",
"Enter cluster description": "Enter cluster description",
"Enter CPU architecture": "Enter CPU architecture",
"Enter description (optional)": "Enter description (optional)",
"Enter display name (optional)": "Enter display name (optional)",
Expand Down Expand Up @@ -1927,6 +1930,7 @@
"IP address": "IP address",
"It will not be destroyed when you perform this action._one": "It will not be destroyed when you perform this action.",
"It will not be destroyed when you perform this action._other": "They will not be destroyed when you perform this action.",
"Italic": "Italic",
"items": "items",
"items per page": "items per page",
"Job": "Job",
Expand Down Expand Up @@ -1990,6 +1994,8 @@
"Let’s get started.": "Let’s get started.",
"Limit the number of clusters selected": "Limit the number of clusters selected",
"Limits": "Limits",
"Link": "Link",
"List": "List",
"List elements": "List elements",
"Loading": "Loading",
"Loading diff": "Loading diff",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ClusterDeploymentK8sResource,
getClusterProperties,
} from '@openshift-assisted/ui-lib/cim'
import { AlertVariant, ButtonVariant, Content, PageSection, Popover } from '@patternfly/react-core'
import { AlertVariant, ButtonVariant, PageSection, Popover } from '@patternfly/react-core'
import { Modal, ModalVariant } from '@patternfly/react-core/deprecated'
import { ExternalLinkAltIcon, OutlinedQuestionCircleIcon, PencilAltIcon } from '@patternfly/react-icons'
import { Markdown } from '@redhat-cloud-services/rule-components/Markdown'
Expand Down Expand Up @@ -47,6 +47,7 @@ import AIClusterDetails from '../../components/cim/AIClusterDetails'
import AIHypershiftClusterDetails from '../../components/cim/AIHypershiftClusterDetails'
import { ClusterStatusMessageAlert } from '../../components/ClusterStatusMessageAlert'
import { DistributionField } from '../../components/DistributionField'
import { EditDescription } from '../../components/EditDescription'
import { EditLabels } from '../../components/EditLabels'
import { HiveNotification } from '../../components/HiveNotification'
import HypershiftClusterDetails from '../../components/HypershiftClusterDetails'
Expand Down Expand Up @@ -85,6 +86,7 @@ export function ClusterOverviewPageContent() {
const localHubName = useLocalHubName()
const clusterDescriptionAnnotation = 'console.open-cluster-management.io/description'
const [showEditLabels, setShowEditLabels] = useState<boolean>(false)
const [showEditDescription, setShowEditDescription] = useState<boolean>(false)
const [showChannelSelectModal, setShowChannelSelectModal] = useState<boolean>(false)
const [curatorSummaryModalIsOpen, setCuratorSummaryModalIsOpen] = useState<boolean>(false)
const { projects } = useProjects()
Expand Down Expand Up @@ -342,12 +344,20 @@ export function ClusterOverviewPageContent() {
description: {
key: t('Description'),
value: cluster?.annotations?.[clusterDescriptionAnnotation] ? (
<Content>
<Markdown template={cluster.annotations[clusterDescriptionAnnotation]} />
</Content>
<Markdown template={cluster.annotations[clusterDescriptionAnnotation]} />
) : (
'-'
),
keyAction: cluster?.isManaged && (
<RbacButton
onClick={() => setShowEditDescription(true)}
variant={ButtonVariant.plain}
aria-label={t('Edit cluster description')}
rbac={[rbacPatch(ManagedClusterDefinition, undefined, cluster?.name)]}
>
<PencilAltIcon />
</RbacButton>
),
},
}

Expand Down Expand Up @@ -506,6 +516,18 @@ export function ClusterOverviewPageContent() {
displayName={cluster.displayName}
close={() => setShowEditLabels(false)}
/>
<EditDescription
resource={
showEditDescription
? {
...ManagedClusterDefinition,
metadata: { name: cluster.name, annotations: cluster.annotations },
}
: undefined
}
displayName={cluster.displayName}
close={() => setShowEditDescription(false)}
/>
{details}
<AcmDescriptionList
title={t('table.details')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* Copyright Contributors to the Open Cluster Management project */

import { IResource, ManagedClusterApiVersion, ManagedClusterKind } from '../../../../../resources'
import { render, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { mockBadRequestStatus, nockIgnoreApiPaths, nockPatch } from '../../../../../lib/nock-util'
import { EditDescription } from './EditDescription'
import { axe } from 'jest-axe'

const CLUSTER_DESCRIPTION_ANNOTATION = 'console.open-cluster-management.io/description'

const resource: IResource = {
apiVersion: ManagedClusterApiVersion,
kind: ManagedClusterKind,
metadata: {
name: 'test-cluster',
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: 'Initial description',
},
},
}

describe('EditDescription', () => {
beforeEach(() => nockIgnoreApiPaths())

test('renders with existing description', () => {
const { getByDisplayValue } = render(<EditDescription resource={resource} close={() => {}} />)
expect(getByDisplayValue('Initial description')).toBeInTheDocument()
})

test('has zero accessibility defects', async () => {
const { container } = render(<EditDescription resource={resource} close={() => {}} />)
expect(await axe(container)).toHaveNoViolations()
})

test('can update description', async () => {
const { getByLabelText, getByRole } = render(<EditDescription resource={resource} close={() => {}} />)
const textarea = getByLabelText('Description')

userEvent.clear(textarea)
userEvent.type(textarea, 'Updated description text')

const nockScope = nockPatch(
{ apiVersion: resource.apiVersion, kind: resource.kind, metadata: { name: resource.metadata!.name } },
{
metadata: {
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: 'Updated description text',
},
},
}
)

getByRole('button', { name: /save/i }).click()
await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
})

test('can clear description', async () => {
const { getByLabelText, getByRole } = render(<EditDescription resource={resource} close={() => {}} />)
const textarea = getByLabelText('Description')

userEvent.clear(textarea)

const nockScope = nockPatch(
{ apiVersion: resource.apiVersion, kind: resource.kind, metadata: { name: resource.metadata!.name } },
{
metadata: {
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: null,
},
},
}
)

getByRole('button', { name: /save/i }).click()
await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
})

test('formatting buttons insert markdown syntax', async () => {
const { getByLabelText, getByLabelText: getByAriaLabel } = render(
<EditDescription resource={resource} close={() => {}} />
)
const textarea = getByLabelText('Description') as HTMLTextAreaElement

userEvent.clear(textarea)
userEvent.type(textarea, 'test')

textarea.setSelectionRange(0, 4)

const boldButton = getByAriaLabel('Bold')
userEvent.click(boldButton)

await waitFor(() => expect(textarea.value).toBe('**test**'))
})

test('shows errors on save failure', async () => {
const { getByLabelText, getByRole } = render(<EditDescription resource={resource} close={() => {}} />)
const textarea = getByLabelText('Description')

userEvent.clear(textarea)
userEvent.type(textarea, 'New description')

const nockScope = nockPatch(
{ apiVersion: resource.apiVersion, kind: resource.kind, metadata: { name: resource.metadata!.name } },
{
metadata: {
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: 'New description',
},
},
},
mockBadRequestStatus
)

getByRole('button', { name: /save/i }).click()
await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
Comment on lines +103 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Return an actual failure status and assert the alert.

nockPatch treats mockBadRequestStatus as the response body; without a fourth argument, it returns 204. This test currently exercises the success path and never verifies the error UI.

Proposed change
       },
-      mockBadRequestStatus
+      mockBadRequestStatus,
+      400
     )

     getByRole('button', { name: /save/i }).click()
     await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
+    expect(getByText('Bad request.')).toBeInTheDocument()
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nockScope = nockPatch(
{ apiVersion: resource.apiVersion, kind: resource.kind, metadata: { name: resource.metadata!.name } },
{
metadata: {
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: 'New description',
},
},
},
mockBadRequestStatus
)
getByRole('button', { name: /save/i }).click()
await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
const nockScope = nockPatch(
{ apiVersion: resource.apiVersion, kind: resource.kind, metadata: { name: resource.metadata!.name } },
{
metadata: {
annotations: {
[CLUSTER_DESCRIPTION_ANNOTATION]: 'New description',
},
},
},
mockBadRequestStatus,
400
)
getByRole('button', { name: /save/i }).click()
await waitFor(() => expect(nockScope.isDone()).toBeTruthy())
expect(getByText('Bad request.')).toBeInTheDocument()
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/routes/Infrastructure/Clusters/ManagedClusters/components/EditDescription.test.tsx`
around lines 103 - 116, Update the nockPatch setup in the EditDescription test
to pass mockBadRequestStatus as the response status argument rather than the
response body, then assert that the expected error alert is displayed after
clicking the Save button. Keep the existing request-completion assertion and
ensure the test exercises the failed update path.

})

test('works without existing annotations', () => {
const resourceWithoutAnnotations: IResource = {
apiVersion: ManagedClusterApiVersion,
kind: ManagedClusterKind,
metadata: {
name: 'test-cluster',
},
}
const { getByLabelText } = render(<EditDescription resource={resourceWithoutAnnotations} close={() => {}} />)
const textarea = getByLabelText('Description') as HTMLTextAreaElement
expect(textarea.value).toBe('')
})
})
Loading