Merge pull request #28651 from backstage/catalog-inspect-dialog-url

Catalog: retain inspect dialog visibility on page reload
This commit is contained in:
Fredrik Adelöw
2025-01-29 12:02:09 +01:00
committed by GitHub
6 changed files with 73 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-graph': patch
---
Fixed an issue causing the `CatalogGraphCard` to redraw its content whenever the parent component re-renders, resulting in flickering.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-catalog': minor
---
The Entity Page now retains the visibility of the Inspect Dialog after a reload. This allows sharing the URL with the dialog open.
@@ -28,7 +28,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import qs from 'qs';
import React, { MouseEvent, ReactNode, useCallback } from 'react';
import React, { MouseEvent, ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { catalogGraphRouteRef } from '../../routes';
import {
@@ -86,7 +86,7 @@ export const CatalogGraphCard = (
} = props;
const { entity } = useEntity();
const entityName = getCompoundEntityRef(entity);
const entityName = useMemo(() => getCompoundEntityRef(entity), [entity]);
const catalogEntityRoute = useRouteRef(entityRouteRef);
const catalogGraphRoute = useRouteRef(catalogGraphRouteRef);
const navigate = useNavigate();
+2
View File
@@ -685,7 +685,9 @@ export function humanizeEntityRef(
export function InspectEntityDialog(props: {
open: boolean;
entity: Entity;
initialTab?: 'overview' | 'ancestry' | 'colocated' | 'json' | 'yaml';
onClose: () => void;
onSelect?: (tab: string) => void;
}): React_2.JSX.Element | null;
// @public
@@ -24,7 +24,7 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect } from 'react';
import React, { ComponentProps, useEffect } from 'react';
import { AncestryPage } from './components/AncestryPage';
import { ColocatedPage } from './components/ColocatedPage';
import { JsonPage } from './components/JsonPage';
@@ -85,6 +85,19 @@ function a11yProps(index: number) {
};
}
const tabNames: Record<
NonNullable<ComponentProps<typeof InspectEntityDialog>['initialTab']>,
string
> = {
overview: 'Overview',
ancestry: 'Ancestry',
colocated: 'Colocated',
json: 'Raw JSON',
yaml: 'Raw YAML',
} as const;
const tabs = Object.keys(tabNames) as Array<keyof typeof tabNames>;
/**
* A dialog that lets users inspect the low level details of their entities.
*
@@ -93,15 +106,20 @@ function a11yProps(index: number) {
export function InspectEntityDialog(props: {
open: boolean;
entity: Entity;
initialTab?: 'overview' | 'ancestry' | 'colocated' | 'json' | 'yaml';
onClose: () => void;
onSelect?: (tab: string) => void;
}) {
const classes = useStyles();
const [activeTab, setActiveTab] = React.useState(0);
const [activeTab, setActiveTab] = React.useState(
getTabIndex(tabs, props.initialTab),
);
const { t } = useTranslationRef(catalogReactTranslationRef);
useEffect(() => {
setActiveTab(0);
}, [props.open]);
getTabIndex(tabs, props.initialTab);
}, [props.open, props.initialTab]);
if (!props.entity) {
return null;
@@ -125,15 +143,16 @@ export function InspectEntityDialog(props: {
orientation="vertical"
variant="scrollable"
value={activeTab}
onChange={(_, newValue) => setActiveTab(newValue)}
onChange={(_, tabIndex) => {
setActiveTab(tabIndex);
props.onSelect?.(tabs[tabIndex]);
}}
aria-label="Inspector options"
className={classes.tabs}
>
<Tab label="Overview" {...a11yProps(0)} />
<Tab label="Ancestry" {...a11yProps(1)} />
<Tab label="Colocated" {...a11yProps(2)} />
<Tab label="Raw JSON" {...a11yProps(3)} />
<Tab label="Raw YAML" {...a11yProps(4)} />
{tabs.map((tab, index) => (
<Tab key={tab} label={tabNames[tab]} {...a11yProps(index)} />
))}
</Tabs>
<TabPanel value={activeTab} index={0}>
@@ -161,3 +180,10 @@ export function InspectEntityDialog(props: {
</Dialog>
);
}
function getTabIndex(
allTabs: string[],
initialTab: keyof typeof tabNames | undefined,
) {
return initialTab ? allTabs.indexOf(initialTab) : 0;
}
@@ -56,8 +56,8 @@ import Box from '@material-ui/core/Box';
import { makeStyles } from '@material-ui/core/styles';
import { TabProps } from '@material-ui/core/Tab';
import Alert from '@material-ui/lab/Alert';
import React, { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import React, { ComponentProps, useEffect, useState } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import useAsync from 'react-use/esm/useAsync';
import { catalogTranslationRef } from '../../alpha/translation';
import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes';
@@ -284,15 +284,15 @@ export const EntityLayout = (props: EntityLayoutProps) => {
);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false);
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const catalogRoute = useRouteRef(rootRouteRef);
const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef);
const { t } = useTranslationRef(catalogTranslationRef);
const cleanUpAfterRemoval = async () => {
setConfirmationDialogOpen(false);
setInspectionDialogOpen(false);
navigate(
unregisterRedirectRoute ? unregisterRedirectRoute() : catalogRoute(),
);
@@ -318,10 +318,12 @@ export const EntityLayout = (props: EntityLayoutProps) => {
// to another entity.
useEffect(() => {
setConfirmationDialogOpen(false);
setInspectionDialogOpen(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
const selectedInspectTab = searchParams.get('inspect');
const showInspectTab = typeof selectedInspectTab === 'string';
return (
<Page themeId={entity?.spec?.type?.toString() ?? 'home'}>
<Header
@@ -353,7 +355,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
onUnregisterEntity={() => setConfirmationDialogOpen(true)}
onInspectEntity={() => setInspectionDialogOpen(true)}
onInspectEntity={() => setSearchParams('inspect')}
/>
</>
)}
@@ -385,17 +387,26 @@ export const EntityLayout = (props: EntityLayoutProps) => {
</Content>
)}
{showInspectTab && (
<InspectEntityDialog
entity={entity!}
initialTab={
(selectedInspectTab as ComponentProps<
typeof InspectEntityDialog
>['initialTab']) || undefined
}
onSelect={newTab => setSearchParams(`inspect=${newTab}`)}
open
onClose={() => setSearchParams()}
/>
)}
<UnregisterEntityDialog
open={confirmationDialogOpen}
entity={entity!}
onConfirm={cleanUpAfterRemoval}
onClose={() => setConfirmationDialogOpen(false)}
/>
<InspectEntityDialog
open={inspectionDialogOpen}
entity={entity!}
onClose={() => setInspectionDialogOpen(false)}
/>
</Page>
);
};