Skip to content

FLPATH-4773: Reflect change introduced by environment agent to UI - #4464

Open
asmasarw wants to merge 2 commits into
redhat-developer:mainfrom
asmasarw:feature/agents-tab
Open

FLPATH-4773: Reflect change introduced by environment agent to UI#4464
asmasarw wants to merge 2 commits into
redhat-developer:mainfrom
asmasarw:feature/agents-tab

Conversation

@asmasarw

@asmasarw asmasarw commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Replace Providers tab with Agents tab

Removes all Providers-related code and introduces a new Agents tab as the default landing tab in the DCM plugin.

dcm-common

  • Deleted ProvidersApi, ProvidersClient, ProvidersClient.test.ts, and types/providers.ts
  • Added AgentsApi, AgentsClient, AgentsClient.test.ts, and types/agents.ts with types derived from the Agent API OpenAPI spec (v1alpha1): Agent, AgentList, AgentRegistrationRequest, AgentCost, AgentHealthStatus, HeartbeatRequest

dcm (frontend plugin)

  • Replaced providersApiRef / providersRouteRef with agentsApiRef / agentsRouteRef
  • Deleted the pages/providers/ directory (all provider UI components)
  • Added pages/agents/ with:
    • AgentsTabContent — paginated table with health-status filtering and agent registration dialog
    • AgentFormFields — registration form backed by service types fetched live from the Catalog API
    • AgentHealthStatus — status badge component
    • CopyButton — reusable clipboard copy component
    • agentFormTypes — form state, Yup validation, and mapping helpers
  • Updated all translation files (ref.ts, de, es, fr, it, ja) to replace providers.* keys with agents.* keys
  • Updated API report files for both packages
  • Added a changeset (minor bump for both packages)

@rhdh-gh-app

rhdh-gh-app Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Missing Changesets

The following package(s) are changed by this PR but do not have a changeset:

  • @red-hat-developer-hub/backstage-plugin-bui-test
  • @red-hat-developer-hub/backstage-plugin-theme

See CONTRIBUTING.md for more information about how to add changesets.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-dcm-common workspaces/dcm/plugins/dcm-common major v1.0.0
@red-hat-developer-hub/backstage-plugin-dcm workspaces/dcm/plugins/dcm major v1.0.0
@red-hat-developer-hub/backstage-plugin-bui-test workspaces/theme/plugins/bui-test none v0.5.0
@red-hat-developer-hub/backstage-plugin-theme workspaces/theme/plugins/theme none v1.1.0

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.91960% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.65%. Comparing base (c7f1b5d) to head (b79f169).
⚠️ Report is 43 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4464      +/-   ##
==========================================
+ Coverage   58.62%   58.65%   +0.03%     
==========================================
  Files        2547     2547              
  Lines      101638   101668      +30     
  Branches    28588    28583       -5     
==========================================
+ Hits        59585    59637      +52     
+ Misses      40252    40229      -23     
- Partials     1801     1802       +1     
Flag Coverage Δ *Carryforward flag
adoption-insights 84.55% <ø> (ø) Carriedforward from c7f1b5d
ai-integrations 67.70% <ø> (ø) Carriedforward from c7f1b5d
app-defaults 48.37% <ø> (ø) Carriedforward from c7f1b5d
augment 46.67% <ø> (ø) Carriedforward from c7f1b5d
boost 79.84% <ø> (ø) Carriedforward from c7f1b5d
bulk-import 72.79% <ø> (ø) Carriedforward from c7f1b5d
cost-management 13.55% <ø> (ø) Carriedforward from c7f1b5d
dcm 73.47% <83.91%> (+1.37%) ⬆️
e2e-adoption-insights 60.00% <ø> (ø) Carriedforward from c7f1b5d
e2e-extensions 62.13% <ø> (ø) Carriedforward from c7f1b5d
e2e-global-header 49.45% <ø> (ø) Carriedforward from c7f1b5d
e2e-homepage 61.11% <ø> (ø) Carriedforward from c7f1b5d
e2e-intelligent-assistant 46.68% <ø> (ø) Carriedforward from c7f1b5d
e2e-orchestrator 49.52% <ø> (ø) Carriedforward from c7f1b5d
e2e-quickstart 55.21% <ø> (ø) Carriedforward from c7f1b5d
e2e-scorecard 50.21% <ø> (ø) Carriedforward from c7f1b5d
e2e-theme 16.36% <ø> (ø) Carriedforward from c7f1b5d
extensions 56.59% <ø> (ø) Carriedforward from c7f1b5d
global-floating-action-button 71.18% <ø> (ø) Carriedforward from c7f1b5d
global-header 66.50% <ø> (ø) Carriedforward from c7f1b5d
homepage 48.39% <ø> (ø) Carriedforward from c7f1b5d
install-dynamic-plugins 58.57% <ø> (ø) Carriedforward from c7f1b5d
intelligent-assistant 75.35% <ø> (ø) Carriedforward from c7f1b5d
konflux 91.98% <ø> (ø) Carriedforward from c7f1b5d
lightspeed 69.02% <ø> (ø) Carriedforward from c7f1b5d
mcp-integrations 84.14% <ø> (ø) Carriedforward from c7f1b5d
orchestrator 70.87% <ø> (ø) Carriedforward from c7f1b5d
quickstart 63.74% <ø> (ø) Carriedforward from c7f1b5d
sandbox 79.56% <ø> (ø) Carriedforward from c7f1b5d
scorecard 87.40% <ø> (ø) Carriedforward from c7f1b5d
theme 88.91% <ø> (ø) Carriedforward from c7f1b5d
translations 5.12% <ø> (ø) Carriedforward from c7f1b5d
x2a 12.40% <ø> (ø) Carriedforward from c7f1b5d

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c7f1b5d...b79f169. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@asmasarw asmasarw changed the title Implement Agents and remove Providers FLPATH-4773: Reflect change introduced by environment agent to UI Aug 25, 2026

@mareklibra mareklibra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A few comments, nothing serious.

const crud = usePaginatedCrudTab<Agent, AgentForm>({
loadFn: ({ pageToken, pageSize: ps }) =>
agentsApi
.listAgents({ page_token: pageToken, max_page_size: ps })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR description and changeset say this tab supports health-status filtering, but listAgents is only called with pagination params and there is no filter UI.

Please either:

  1. Add a health-status filter (and pass health_status into listAgents), or
  2. Drop that claim from the changeset / PR body so the shipped behavior matches the docs.

The client already supports the query param, so wiring a select/chips control should be small.

);
}

describe('AgentsTabContent', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR description and changeset say this tab supports health-status filtering, but listAgents is only called with pagination params and there is no filter UI.

Please either:

  1. Add a health-status filter (and pass health_status into listAgents), or
  2. Drop that claim from the changeset / PR body so the shipped behavior matches the docs.

The client already supports the query param, so wiring a select/chips control should be small.

@@ -0,0 +1,14 @@
---
'@red-hat-developer-hub/backstage-plugin-dcm': minor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there already any production use of the DCM?

This removes @public exports (ProvidersApi, ProvidersClient, providersApiRef, providersRouteRef) from packages already at v1.0.0. That is a breaking change.

Please bump both packages to major unless DCM 1.0.0 is explicitly unpublished / internal-only and that is documented in the changeset.

Also please mention the removed public symbols in the changeset so downstream consumers are not surprised.

const catalogApi = useApi(catalogApiRef);
const { t } = useTranslation();

const {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

useInfiniteSelect already exposes loading for the first page, but it is unused. AgentFormFields only spins on loadingMoreServiceTypes, so the first fetch looks like “no service types”.

Please pass loading through and show the same CircularProgress MenuItem while the first page is in flight.

},
instances: {
emptyTitle: 'No instances provisioned',
emptyDescription:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

instances.emptyDescription still says “registered provider infrastructure”. With Providers removed, please reword (e.g. “registered environment agents”) and update the locale files to match.

'At least one service type is required',
),
),
cost: yup

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

cost is only .required(), so any non-empty string passes isAgentFormValid. Please constrain it to AGENT_COST_OPTIONS:

cost: yup
  .string()
  .oneOf(AGENT_COST_OPTIONS, m('validation.agent.costRequired', 'Cost is required'))
  .required(m('validation.agent.costRequired', 'Cost is required')),

deleteProvider(providerId: string): Promise<void>;
export interface AgentsApi {
listAgents(
params?: PaginationParams & { health_status?: string },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agent.health_status is typed as AgentHealthStatus, but the list filter is still health_status?: string. Please use the same union so callers cannot pass arbitrary values:

listAgents(
  params?: PaginationParams & { health_status?: AgentHealthStatus },
): Promise<AgentList>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same change in AgentsClient.listAgents (line 41).

),
},
{
title: t('agents.columns.health'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

last_heartbeat is on the Agent model but never shown. A “Last heartbeat” column next to Health (or a tooltip on the badge) would make unavailable vs a stale ready easier to diagnose. Other tabs already render timestamps the same way, e.g.:

{
  title: t('agents.columns.lastHeartbeat'),
  field: 'last_heartbeat',
  render: a =>
    a.last_heartbeat ? (
      <Typography variant="body2">
        {new Date(a.last_heartbeat).toLocaleString()}
      </Typography>
    ) : (
      <DcmEmptyCell />
    ),
},

Optional: in AgentFormFields, default topic_name to dcm.agent.${name} while the user is still on the generated value, so they do not have to retype the prefix.

onPrimaryAction={crud.handleOpenCreate}
illustrationSrc={emptyIllustration}
entityLabel={t('agents.entityLabel')}
actionError={serviceTypesError}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor consistency nit vs Providers / Catalog items: they pass onDismissActionError so the service-type load alert can be closed. Here only actionError={serviceTypesError} is set, so MUI renders a non-dismissable banner.

useInfiniteSelect has no clearError, so a local flag is enough:

const [serviceTypesErrorDismissed, setServiceTypesErrorDismissed] = useState(false);
// ...
actionError={serviceTypesErrorDismissed ? null : serviceTypesError}
onDismissActionError={() => setServiceTypesErrorDismissed(true)}

Not blocking - sticky is reasonable for a failed catalog fetch. Nice to match the other tabs.

@asmasarw
asmasarw requested a review from mareklibra August 31, 2026 10:27

@mareklibra mareklibra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

More comments

onPrimaryAction={crud.handleOpenCreate}
illustrationSrc={emptyIllustration}
entityLabel={t('agents.entityLabel')}
toolbarExtra={healthFilterControl}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Health filter on empty results is a dead end. DcmCrudTabLayout only renders toolbarExtra in the table branch; a health filter that returns 0 agents hits items.length === 0 && !hasPrev and shows the global empty state (“No agents registered”) with no way to change the filter short of a full reload.

Please either:

  1. Render toolbarExtra in the empty-state branch as well, and use different copy when a filter is active, or
  2. Skip the illustration empty state when healthFilter is set and keep the table + select visible.

resetAndReload() already clears the token stack, so hasPrev will not save this path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The empty-state branch now also renders toolbarExtra (with the filter Select) before the action error alert and illustration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See AgentsTabCOntent.tsxx:200 comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if (items.length === 0 && !cursorPagination?.hasPrev) {
return (
<>
{toolbarExtra && (
<Box className={classes.toolbarRow}>{toolbarExtra}</Box>
)}
{/* existing actionError + DcmDataCenterTabEmptyState */}
</>
);
}

loadMoreServiceTypes?: () => void;
}>;

export function AgentFormFields({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The Providers form and CopyButton unit tests were deleted and not replaced. AgentsTabContent tests cover list/pagination/filter but never open the register dialog, and usePaginatedCrudTab.test.ts does not cover resetAndReload (the helper the filter depends on).

Please port the old ProviderFormFields / CopyButton cases onto AgentFormFields / CopyButton, and add a resetAndReload test that asserts pageToken is undefined and hasPrev is false after goNext().

value: AgentHealthStatus;
label: string;
}> = [
{ value: 'ready', label: 'Ready' },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i18n

title: t('agents.columns.serviceTypes'),
field: 'service_types',
sorting: false,
render: a => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If service_types is [] the column renders an empty Box. Other tabs use for that. Please add the same fallback before the chip list.


type CopyState = 'idle' | 'copied' | 'failed';

export function CopyButton({ text }: Readonly<{ text: string }>) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This CopyButton no longer swaps the icon on copied/failed (the Providers version used CheckIcon / ErrorOutlineIcon). Tooltip-only feedback is easy to miss. Please restore the icon states or move CopyButton to components/ so every tab shares one implementation.

helperText={err('name') ?? t('agents.form.nameHelper')}
error={Boolean(err('name'))}
value={form.name}
onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

while topic_name is still the generated value, default it to dcm.agent.${name} on name change so users do not have to retype the prefix. Stop updating once they edit the topic field.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still uses still uses Provider / listProviders / storageKey: 'providers'.

@asmasarw
asmasarw requested a review from mareklibra September 1, 2026 11:09
// While false, typing in the name field auto-fills topic_name to
// `dcm.agent.<name>`. Once the user edits topic_name directly the
// auto-fill is disabled for the lifetime of this form instance.
const topicManuallyEdited = useRef(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

topicManuallyEdited survives dialog close because AgentFormFields stays mounted (DcmFormDialog always renders children) while useCrudTab only resets createForm.

Repro: open Register → type a name (topic auto-fills) → edit topic → Cancel → open Register again → type a name. Topic stays empty.

Please reset the flag when the form is cleared, remount the fields (key on open), or derive “still generated” from topic_name === \dcm.agent.${name}`` instead of a ref.

// on the wrapping Tooltip span via title attribute propagation.
// A simpler check: the error icon must NOT be present.
// After success the error icon must NOT be present
expect(screen.queryByTestId('ErrorOutlineIcon')).not.toBeInTheDocument();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

these cases do not actually assert the icon swap

Success never looks for CheckIcon. Failure is ErrorOutlineIcon || .MuiSvgIcon-root, which is true for the idle copy icon too.

Please assert CheckIcon after resolve and ErrorOutlineIcon after reject (and drop the .MuiSvgIcon-root fallback).

<TabbedLayout>
<TabbedLayout.Route path="/" title={t('page.tabs.providers')}>
<ProvidersTabContent />
<TabbedLayout.Route path="/" title={t('page.tabs.agents')}>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FLPATH-4773 still says rename SP → Environments and mark resources degraded when the agent is gone. This change uses “Agents” and the Resources column is still “Provider”. If that work is out of scope, please point at the follow-up issue so the story is not silently closed.

expect(isAgentFormValid({ ...VALID_FORM, service_types: [] })).toBe(false);
});

it('returns false when cost is empty', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please add isAgentFormValid({ ...VALID_FORM, cost: 'not-a-cost' as AgentForm['cost'] }) === false so the oneOf constraint cannot regress.

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@asmasarw
asmasarw requested a review from mareklibra September 1, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants