Skip to content

feat: Swap mapping direction#107

Merged
sandornagy517 merged 12 commits intoservice_mappigns_featurefrom
feat/swap_mapping_direction
Jan 27, 2026
Merged

feat: Swap mapping direction#107
sandornagy517 merged 12 commits intoservice_mappigns_featurefrom
feat/swap_mapping_direction

Conversation

@sandornagy517
Copy link
Contributor

Description

To improve loading performance of the mappings page we swap the direction of the data loaded. Until now were were mapping Backstage entities to PagerDuty services, now we swap this and will do the opposite. In this PR I've updated the mappings table to the new Backstage UI table and updated the way we load the data for the component.

image

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.

@sandornagy517 sandornagy517 requested a review from a team as a code owner December 10, 2025 13:48
@sandornagy517 sandornagy517 changed the base branch from main to next December 10, 2025 13:48
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 comments. The PR is quite big (and visual), so probably I would need to test it locally to make more sense of it.

await Promise.all(
Object.entries(EndpointConfig).map(async ([account, _]) => {
services = await Promise.all(
ids.map(async id => await getServiceById(id, account)),
Copy link
Contributor

Choose a reason for hiding this comment

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

mapping => mapping.serviceId === service.id,
e =>
e.integrationKey ===
ent?.metadata.annotations?.['pagerduty.com/integration-key'],
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any guarantees that at this point the integration-key was set? 🤔 Why not searching by integration-key OR service-id.

Comment on lines +232 to +243
result.mappings.push({
entityRef: '',
entityName: '',
integrationKey: entityMapping?.integrationKey,
serviceId: entityMapping?.serviceId ?? '',
status: 'NotMapped',
serviceName: '',
team: '',
escalationPolicy: '',
serviceUrl: '',
account: '',
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to push this empty state?

});

// GET /mapping/entity
router.get('/mapping/entity', async (_, response) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please don't remove any methods. Instead mark them as deprecated. There is a chance that people only update the frontend or just the backend and then there will be missing methods.

search: debouncedSearchQuery,
searchFields: ['metadata.name', 'spec.owner'],
}),
staleTime: 5 * 60 * 1000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why 5 minutes? 🤔 Is it worth caching at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm, not sure why did I include this back then, just removed it!

const [pageSize, setPageSize] = useState(10);
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
useDebounce(() => setDebouncedSearchQuery(searchQuery), 500, [searchQuery]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use debounce used like this is so awkward... Shouldn't it return something? What's the point of the hook at all? 🤔

}
}

export async function getServicesByIdsServiceDirectory(
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 need to expose the calling service from PagerDuty. getServicesByIdsAndAccount would be more readable and not expose the fact that the call is made to ServicesDirectory.

};

const formattedEntity = {
name: entity?.metadata?.name,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would the entity be null? Doesn't it come from the catalog API? Also, if it is null would be better practice to early return here, instead of dragging the ? around forever?

Comment on lines +801 to +806
const foundServices = pagerDutyServices.filter(
s => s.id === serviceId,
);
if (foundServices.length > 0) {
service = foundServices[0];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const foundServices = pagerDutyServices.filter(
s => s.id === serviceId,
);
if (foundServices.length > 0) {
service = foundServices[0];
}
service = pagerDutyServices.find(s => s.id === serviceId);

account,
);
} catch (e) {
// Service might not exist, just continue
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 this type of error is only valid if the error is a 404. Otherwise you will not know if there is a map or not and could decide to map something else.

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.

LGTM! Let's just make some tests in the ServiceMappingComponent (I think it is the topmost component) and test the router.ts new endpoint (which I also think is the topmost backend piece of software). There is no tests in the PR and I think there should be.

@sandornagy517 sandornagy517 force-pushed the feat/swap_mapping_direction branch from c38fcb1 to ed2ba57 Compare January 23, 2026 10:54
@sandornagy517 sandornagy517 force-pushed the feat/swap_mapping_direction branch from ed2ba57 to 582e3ba Compare January 23, 2026 11:01
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 comments :)

describe('createRouter', () => {
let app: express.Express;
let store: PagerDutyBackendStore;
const mockCatalogApi = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to mock the entire class? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mocked it for TS reasons only

expect(response.status).toEqual(200);
expect(response.body).toHaveProperty('entities');
expect(response.body).toHaveProperty('totalCount');
expect(Array.isArray(response.body.entities)).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Well, [ ] is also an array, that doesn't make this test more accurate. I think we should check if the responde contains the expected mapping, and not just check if it is an array. Otherwise I can change the implementation of the controller to just return an empty array and it will pass anyway.


expect(response.status).toEqual(200);
expect(response.body).toHaveProperty('entities');
expect(response.body).toHaveProperty('totalCount');
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, we're not asserting the return of the request, and we should.

Comment on lines +2467 to +2470
expect(response.status).toEqual(200);
expect(response.body).toHaveProperty('entities');
expect(response.body).toHaveProperty('totalCount');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Ergh... these tests look all the same thing. The fact that we mock everything in the mockCatalogApi doesn't allow for testing much. The fact that you never assert the contents of entities is the prove itself that the test doesn't really matter. Tbh, unless we change these tests to actually alter the catalog that is queried, I'd even prefer to just wipe these tests. They're not testing anything in my opinion.


expect(response.status).toEqual(200);
expect(response.body).toHaveProperty('entities');
expect(response.body).toHaveProperty('totalCount');
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, not testing anything.

Comment on lines +202 to +208
await act(async () => {
fireEvent.change(searchInput, { target: { value: 'my-component' } });
});

await act(async () => {
jest.advanceTimersByTime(500);
});
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 modern RTL tests still require act wrapping. fireEvent is already wrapped in a act. For advanceTimersByTime you may be need, because it is not a RTL event.

expect(screen.getByText('Out of Sync')).toBeInTheDocument();
expect(screen.getByText('Not Mapped')).toBeInTheDocument();
expect(
screen.getByText('Error occured while fetching service'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this fit the table column? 🤔

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 does not but gets dotted out, on hover the full text is displayed

@sandornagy517 sandornagy517 changed the base branch from next to service_mappigns_feature January 27, 2026 13:09
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.

LGTM 🚀

@sandornagy517 sandornagy517 merged commit 69099f2 into service_mappigns_feature Jan 27, 2026
15 checks passed
sandornagy517 added a commit that referenced this pull request Mar 11, 2026
* feat: Swap service mapping direction

* feat: Get rid of the old MappingTable

* feat: Use a different API for retrieving PD services

* fix: Revert entity mappings

* fix: Resolve conflicts

* fix: Service mapping creation and update

* feat: Add unit tests

* fix: Remove unused import

* fix: Add proper expects for router unit tests

* fix: Get rid of redundant unit test expects

* fix: Mock catalog api the backstage way
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.

2 participants