Skip to content

feat: enhance mapping dialog UI with team filtering and improved styling#139

Open
pdt-rmunoz wants to merge 5 commits intoservice_mappigns_featurefrom
pdt-rmunoz/mapping-dialog-enhancement-v2
Open

feat: enhance mapping dialog UI with team filtering and improved styling#139
pdt-rmunoz wants to merge 5 commits intoservice_mappigns_featurefrom
pdt-rmunoz/mapping-dialog-enhancement-v2

Conversation

@pdt-rmunoz
Copy link
Contributor

Description

This commit adds comprehensive improvements to the mapping dialog:

Backend changes:

  • Add getAllTeams endpoint to fetch all PagerDuty teams
  • Add getFilteredServices endpoint with team_id and query filters
  • Add PagerDutyTeam type to common types

Frontend changes:

  • Add team filtering dropdown to filter services by PagerDuty team
  • Add service search with debounced input
  • Replace read-only text displays with TextField components for visual consistency
  • Add border container styling to RadioGroup matching TextField appearance
  • Add enhanced radio button styling with interactive states
image image image

Issue number:
https://pagerduty.atlassian.net/browse/DEVECO-527

Affected plugin

  • backstage-plugin
  • backstage-plugin-backend
  • backstage-plugin-scaffolder-actions
  • backstage-plugin-entity-processor

Type of change

  • New feature (non-breaking change which adds functionality)
  • Fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • I have performed a self-review of this change
  • Changes have been tested
  • Changes have been tested in dark theme
  • Changes are documented
  • Changes generate no new warnings
  • PR title follows conventional commit semantics

If this is a breaking change 👇

  • I have documented the migration process
  • I have implemented necessary warnings (if it can live side by side)

Acknowledgement

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

This commit adds comprehensive improvements to the mapping dialog:

Backend changes:
- Add getAllTeams endpoint to fetch all PagerDuty teams
- Add getFilteredServices endpoint with team_id and query filters
- Add PagerDutyTeam type to common types

Frontend changes:
- Add team filtering dropdown to filter services by PagerDuty team
- Add service search with debounced input (500ms)
- Replace read-only text displays with TextField components for visual consistency
- Add border container styling to RadioGroup matching TextField appearance
- Add enhanced radio button styling with interactive states

Radio button improvements:
- Individual padding on each option (12px all sides)
- Bottom border dividers between options
- Hover effect with gray background
- Selected state with blue background, bold text, and 3px left accent bar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pdt-rmunoz pdt-rmunoz requested a review from a team as a code owner February 25, 2026 00:46
Copy link
Contributor

@sandornagy517 sandornagy517 left a comment

Choose a reason for hiding this comment

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

great job! just left some small comments

Object.keys(EndpointConfig).map(async account => {
try {
// reset offset value
offset = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

these comments are a bit redundant, could you please remove them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

solve it

let params = `time_zone=UTC&limit=${limit}`;

// Add team_ids filter if provided
if (teamIds && teamIds.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

same for the comments here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

const [searchQuery, setSearchQuery] = useState<string>('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>('');

// Debounce search query (wait 500ms after user stops typing)
Copy link
Contributor

Choose a reason for hiding this comment

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

same for the comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

<Flex>
<Text variant="body-medium" weight="regular">
Name:
<style>
Copy link
Contributor

Choose a reason for hiding this comment

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

we should use makeStyles and useStyles for these

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added import { makeStyles } from '@material-ui/core'
Created useStyles hook with nested selectors

<>
<Box
className="radio-list-container"
style={{
Copy link
Contributor

Choose a reason for hiding this comment

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

same for styles like above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

same as above

- Remove redundant comments from pagerduty.ts functions (getAllTeams, getFilteredServices)
- Remove redundant comments from MappingsDialog.tsx
- Refactor inline <style> tag to use makeStyles from @material-ui/core
- Refactor inline Box styles to use makeStyles pattern
- Follow established codebase pattern for styling (StatusCell, AutoMappingsButton)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@jhfgloria
Copy link
Contributor

Is it me, or the first option is smaller than the others, @pdt-rmunoz?

image

Copy link
Contributor

@jhfgloria jhfgloria left a comment

Choose a reason for hiding this comment

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

Left some questions and comments. Overall really nice PR.

Comment on lines +1104 to +1125
// GET /filtered-services?team_id=:teamId&query=:query&limit=:limit
router.get('/filtered-services', async (request, response) => {
try {
const teamId = request.query.team_id as string | undefined;
const query = request.query.query as string | undefined;
const limit = request.query.limit
? parseInt(request.query.limit as string, 10)
: 100;

const teamIdsArray: string[] | undefined = teamId ? [teamId] : undefined;

const services = await getFilteredServices(teamIdsArray, query, limit);

response.json(services);
} catch (error) {
if (error instanceof HttpError) {
response.status(error.status).json({
errors: [`${error.message}`],
});
}
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should have an extra method here. I think this method is just a specialisation of the next method /services. You should merge them together, and have a different execution path for either a team_id search, or a integration_key search. Otherwise we will end up with a multitude of controller methods for every different query string someone decides to introduce.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this, yes, it was merged to /services, no multitude of controller methods!

const res = await getTeams(offset, limit, account);

res[1].forEach(team => {
team.account = account;
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to be careful about this one. Is this setting it as default? I think I mentioned this to @sandornagy517 before, that we should avoid using the default value, which I believe is the case here.

Copy link
Contributor

Choose a reason for hiding this comment

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

image I've already added this account selector, maybe we should merge that PR first and then adjust this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

solved, implemented

Comment on lines +71 to +77
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);

return () => clearTimeout(timer);
}, [searchQuery]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think @sandornagy517 created a new useDebounce hook that abstracts this login.

Suggested change
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
const debouncedSearchQuery = useDebounced(searchQuery);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

implemented debounce solution as well

Comment on lines +148 to +153
createMapping({
serviceId: currentServiceId,
integrationKey: currentIntegrationKey,
entityRef: '',
account: account,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we shouldn't remove the mapping entirely... 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it would be a great idea, currently just removing the service and it's also the same behavior to remove the mapping manually

value={selectedTeamIds[0] || ''}
onChange={value => {
const teamId = String(value || '');
setSelectedTeamIds(teamId ? [teamId] : []);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we store the team ids as an array. Seems like I can only select one at a time...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, nice catch! simplified!

sandornagy517 and others added 2 commits March 3, 2026 21:33
* feat: Add account selector to the service mappings table

* fix: Revert app-config.yaml

* fix: Revert app-config.yaml

* fix: Revert app-config.yaml

* refactor: Use oneline if
- Pass account parameter through entire mapping flow (frontend → API → backend → PagerDuty)
- Add proper error handling for non-HttpError exceptions in POST /mapping/entity endpoint to prevent indefinite request hangs
- Filter out currently-mapped service from mapping dialog service list
- Remove unused useEffect import from MappingsTable
- Update mock API signatures to match new account parameter requirements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pdt-rmunoz
Copy link
Contributor Author

I bring the changes from @sandornagy517. Please @jhfgloria let me know if it's clear in that way or I bring my three commits to a new clean branch. Thanks

});

// GET /filtered-services?team_id=:teamId&query=:query&limit=:limit
// GET /filtered-services?team_id=:teamId&query=:query&limit=:limit&account=:account
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really like the shape this is getting... We have GET /all-pd-services, GET /filtered-services and GET /services. This should be a single endpoint with multiple fetch approaches depending on the passed query strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, let's align on this. I'm open to following best practices here. I understand we don't want to end up deprecating endpoints in the future—I went with this approach because it seemed more straightforward to manage at the time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

implemented and working as expected, aligned to the best practices and scalable solution

@pdt-rmunoz
Copy link
Contributor Author

Is it me, or the first option is smaller than the others, @pdt-rmunoz?

image

yes, there was a inconsistent CSS variable. Good catch. Fixed

@pdt-rmunoz
Copy link
Contributor Author

Is it me, or the first option is smaller than the others, @pdt-rmunoz?
image

yes, Good catch. I override RadioGroupContent's default gap to remove spacing between radio items

- Replace manual debounce logic with useDebounce hook
- Simplify team selection from array to single string value
- Fix radio list styling inconsistencies (spacing, padding, borders)
- Override RadioGroupContent default gap for consistent item spacing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pdt-rmunoz
Copy link
Contributor Author

Hi @jhfgloria, a new implementation with the optimizations. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants