diff --git a/.changeset/witty-tools-cough.md b/.changeset/witty-tools-cough.md
new file mode 100644
index 0000000000..71b1a8a453
--- /dev/null
+++ b/.changeset/witty-tools-cough.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Migrated `InspectEntityDialog` from Material UI to Backstage UI components. Added new translation keys: `inspectEntityDialog.overviewPage.copyAriaLabel`, `inspectEntityDialog.overviewPage.copiedStatus`, `inspectEntityDialog.overviewPage.helpLinkAriaLabel`, and `inspectEntityDialog.colocatedPage.entityListAriaLabel`.
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 04b05330ff..b8b1e73f98 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -80,6 +80,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^24.0.0",
+ "@remixicon/react": "^4.6.0",
"classnames": "^2.2.6",
"lodash": "^4.17.21",
"material-ui-popup-state": "^5.3.6",
diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md
index 19f1b2ce3a..8cc0c5a84e 100644
--- a/plugins/catalog-react/report-alpha.api.md
+++ b/plugins/catalog-react/report-alpha.api.md
@@ -74,6 +74,7 @@ export const catalogReactTranslationRef: TranslationRef<
readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.';
readonly 'inspectEntityDialog.colocatedPage.locationHeader': 'At the same location';
readonly 'inspectEntityDialog.colocatedPage.originHeader': 'At the same origin';
+ readonly 'inspectEntityDialog.colocatedPage.entityListAriaLabel': 'Colocated entities';
readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON';
readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.';
readonly 'inspectEntityDialog.overviewPage.title': 'Overview';
@@ -83,6 +84,9 @@ export const catalogReactTranslationRef: TranslationRef<
readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity';
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
readonly 'inspectEntityDialog.overviewPage.tags': 'Tags';
+ readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}';
+ readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied';
+ readonly 'inspectEntityDialog.overviewPage.helpLinkAriaLabel': 'Learn more';
readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations';
readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML';
readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.';
diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md
index 419a3c3f94..befbd1faf8 100644
--- a/plugins/catalog-react/report.api.md
+++ b/plugins/catalog-react/report.api.md
@@ -196,6 +196,7 @@ export const catalogReactTranslationRef: TranslationRef<
readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.';
readonly 'inspectEntityDialog.colocatedPage.locationHeader': 'At the same location';
readonly 'inspectEntityDialog.colocatedPage.originHeader': 'At the same origin';
+ readonly 'inspectEntityDialog.colocatedPage.entityListAriaLabel': 'Colocated entities';
readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON';
readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.';
readonly 'inspectEntityDialog.overviewPage.title': 'Overview';
@@ -205,6 +206,9 @@ export const catalogReactTranslationRef: TranslationRef<
readonly 'inspectEntityDialog.overviewPage.identity.title': 'Identity';
readonly 'inspectEntityDialog.overviewPage.annotations': 'Annotations';
readonly 'inspectEntityDialog.overviewPage.tags': 'Tags';
+ readonly 'inspectEntityDialog.overviewPage.copyAriaLabel': 'Copy {{label}}';
+ readonly 'inspectEntityDialog.overviewPage.copiedStatus': 'Copied';
+ readonly 'inspectEntityDialog.overviewPage.helpLinkAriaLabel': 'Learn more';
readonly 'inspectEntityDialog.overviewPage.relation.title': 'Relations';
readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML';
readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.';
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx
index 2b047bf762..fa5e87fcd8 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx
@@ -15,16 +15,16 @@
*/
import { Entity } from '@backstage/catalog-model';
-import Box from '@material-ui/core/Box';
-import Button from '@material-ui/core/Button';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogContent from '@material-ui/core/DialogContent';
-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 { ComponentProps, useEffect, useState, ReactNode, useMemo } from 'react';
+import {
+ Dialog,
+ DialogHeader,
+ DialogBody,
+ Tabs,
+ TabList,
+ Tab,
+ TabPanel,
+} from '@backstage/ui';
+import { useMemo } from 'react';
import { AncestryPage } from './components/AncestryPage';
import { ColocatedPage } from './components/ColocatedPage';
import { JsonPage } from './components/JsonPage';
@@ -33,64 +33,68 @@ import { YamlPage } from './components/YamlPage';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
-const useStyles = makeStyles(theme => ({
- fullHeightDialog: {
- height: 'calc(100% - 64px)',
- },
- root: {
- display: 'flex',
- flexGrow: 1,
- width: '100%',
- backgroundColor: theme.palette.background.paper,
- },
- tabs: {
- borderRight: `1px solid ${theme.palette.divider}`,
- flexShrink: 0,
- },
- tabContents: {
- flexGrow: 1,
- overflowX: 'auto',
- },
-}));
-
-function TabPanel(props: {
- children?: ReactNode;
- index: number;
- value: number;
-}) {
- const { children, value, index, ...other } = props;
- const classes = useStyles();
- return (
-
- {value === index && (
-
- {children}
-
- )}
-
- );
-}
-
-function a11yProps(index: number) {
- return {
- id: `vertical-tab-${index}`,
- 'aria-controls': `vertical-tabpanel-${index}`,
- };
-}
-
type TabKey = 'overview' | 'ancestry' | 'colocated' | 'json' | 'yaml';
-type TabNames = Record<
- NonNullable['initialTab']>,
- string
->;
+const TAB_KEYS: TabKey[] = [
+ 'overview',
+ 'ancestry',
+ 'colocated',
+ 'json',
+ 'yaml',
+];
+
+function DialogContents(props: {
+ entity: Entity;
+ initialTab?: TabKey;
+ onSelect?: (tab: string) => void;
+}) {
+ const { entity, initialTab, onSelect } = props;
+ const { t } = useTranslationRef(catalogReactTranslationRef);
+
+ const tabNames = useMemo(
+ () => ({
+ overview: t('inspectEntityDialog.tabNames.overview'),
+ ancestry: t('inspectEntityDialog.tabNames.ancestry'),
+ colocated: t('inspectEntityDialog.tabNames.colocated'),
+ json: t('inspectEntityDialog.tabNames.json'),
+ yaml: t('inspectEntityDialog.tabNames.yaml'),
+ }),
+ [t],
+ );
+
+ const tabContent: Record = {
+ overview: ,
+ ancestry: ,
+ colocated: ,
+ json: ,
+ yaml: ,
+ };
+
+ return (
+ <>
+ {t('inspectEntityDialog.title')}
+
+ onSelect?.(key as string)}
+ >
+
+ {TAB_KEYS.map(tab => (
+
+ {tabNames[tab]}
+
+ ))}
+
+ {TAB_KEYS.map(tab => (
+
+ {tabContent[tab]}
+
+ ))}
+
+
+ >
+ );
+}
/**
* A dialog that lets users inspect the low level details of their entities.
@@ -104,90 +108,26 @@ export function InspectEntityDialog(props: {
onClose: () => void;
onSelect?: (tab: string) => void;
}) {
- const classes = useStyles();
- const { t } = useTranslationRef(catalogReactTranslationRef);
+ const { open, entity, initialTab, onClose, onSelect } = props;
- const tabNames: TabNames = useMemo(
- () => ({
- overview: t('inspectEntityDialog.tabNames.overview'),
- ancestry: t('inspectEntityDialog.tabNames.ancestry'),
- colocated: t('inspectEntityDialog.tabNames.colocated'),
- json: t('inspectEntityDialog.tabNames.json'),
- yaml: t('inspectEntityDialog.tabNames.yaml'),
- }),
- [t],
- );
-
- const tabs = Object.keys(tabNames) as TabKey[];
-
- const [activeTab, setActiveTab] = useState(
- getTabIndex(tabs, props.initialTab),
- );
-
- useEffect(() => {
- getTabIndex(tabs, props.initialTab);
- }, [props.open, props.initialTab, tabs]);
-
- if (!props.entity) {
+ if (!entity) {
return null;
}
return (
);
}
-
-function getTabIndex(allTabs: string[], initialTab: TabKey | undefined) {
- return initialTab ? allTabs.indexOf(initialTab) : 0;
-}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
index ec7c0dd54f..d7b36882e8 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
@@ -27,8 +27,7 @@ import {
ResponseErrorPanel,
} from '@backstage/core-components';
import { useApi, useApp, useRouteRef } from '@backstage/core-plugin-api';
-import Box from '@material-ui/core/Box';
-import DialogContentText from '@material-ui/core/DialogContentText';
+import { Text, Box } from '@backstage/ui';
import { makeStyles } from '@material-ui/core/styles';
import classNames from 'classnames';
import { useLayoutEffect, useRef, useState } from 'react';
@@ -201,19 +200,18 @@ export function AncestryPage(props: { entity: Entity }) {
return (
<>
-
- {t('inspectEntityDialog.ancestryPage.title')}
-
-
- {t('inspectEntityDialog.ancestryPage.description', {
- processorsLink: (
-
- {t('inspectEntityDialog.ancestryPage.processorsLink')}
-
- ),
- })}
-
-
+
+
+ {t('inspectEntityDialog.ancestryPage.description', {
+ processorsLink: (
+
+ {t('inspectEntityDialog.ancestryPage.processorsLink')}
+
+ ),
+ })}
+
+
+
and are used intentionally for
+// semantic markup. The react/forbid-elements rule predates the BUI migration.
+/* eslint-disable react/forbid-elements */
+
import {
Entity,
ANNOTATION_LOCATION,
@@ -22,22 +26,40 @@ import {
} from '@backstage/catalog-model';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
-import DialogContentText from '@material-ui/core/DialogContentText';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import { makeStyles } from '@material-ui/core/styles';
-import Alert from '@material-ui/lab/Alert';
+import { Text, Alert } from '@backstage/ui';
import useAsync from 'react-use/esm/useAsync';
import { catalogApiRef } from '../../../api';
import { EntityRefLink } from '../../EntityRefLink';
-import { KeyValueListItem, ListItemText } from './common';
+import { makeStyles } from '@material-ui/core/styles';
+import { ListSection, ListItemRow } from './common';
+
import { catalogReactTranslationRef } from '../../../translation';
+
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles({
- root: {
- display: 'flex',
- flexDirection: 'column',
+ header: {
+ paddingLeft: 'var(--bui-space-4)',
+ marginTop: 'var(--bui-space-4)',
+ marginBottom: 'var(--bui-space-4)',
+ },
+ headerLabel: {
+ margin: 0,
+ fontFamily: 'monospace',
+ fontSize: 'var(--bui-font-size-3)',
+ fontWeight: 'var(--bui-font-weight-regular)' as any,
+ },
+ entityList: {
+ marginTop: 'var(--bui-space-4)',
+ },
+ headerValue: {
+ margin: 0,
+ marginTop: 'var(--bui-space-1)',
+ fontFamily: 'monospace',
+ fontSize: 'var(--bui-font-size-3)',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
},
});
@@ -63,7 +85,11 @@ function useColocated(entity: Entity): {
? [{ [`metadata.annotations.${ANNOTATION_LOCATION}`]: location }]
: []),
...(origin
- ? [{ [`metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`]: origin }]
+ ? [
+ {
+ [`metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`]: origin,
+ },
+ ]
: []),
],
});
@@ -82,15 +108,27 @@ function useColocated(entity: Entity): {
}
function EntityList(props: { entities: Entity[]; header?: [string, string] }) {
+ const classes = useStyles();
+ const { t } = useTranslationRef(catalogReactTranslationRef);
return (
-
- {props.header && }
- {props.entities.map(entity => (
-
- } />
-
- ))}
-
+ <>
+ {props.header && (
+
+ {props.header[0]}
+ {props.header[1]}
+
+ )}
+
+ {props.entities.map(entity => (
+
+
+
+ ))}
+
+ >
);
}
@@ -108,15 +146,19 @@ function Contents(props: { entity: Entity }) {
if (!location && !originLocation) {
return (
-
- {t('inspectEntityDialog.colocatedPage.alertNoLocation')}
-
+
);
} else if (!colocatedEntities?.length) {
return (
-
- {t('inspectEntityDialog.colocatedPage.alertNoEntity')}
-
+
);
}
@@ -157,19 +199,11 @@ function Contents(props: { entity: Entity }) {
}
export function ColocatedPage(props: { entity: Entity }) {
- const classes = useStyles();
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
-
- {t('inspectEntityDialog.colocatedPage.title')}
-
-
- {t('inspectEntityDialog.colocatedPage.description')}
-
-
-
-
+ {t('inspectEntityDialog.colocatedPage.description')}
+
>
);
}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx
index b5cfdfa219..e238cd0475 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { CodeSnippet } from '@backstage/core-components';
-import DialogContentText from '@material-ui/core/DialogContentText';
+import { Text } from '@backstage/ui';
import { sortKeys } from './util';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -25,21 +25,14 @@ export function JsonPage(props: { entity: Entity }) {
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
-
- {t('inspectEntityDialog.jsonPage.title')}
-
-
- {t('inspectEntityDialog.jsonPage.description')}
-
-
-
-
-
-
+ {t('inspectEntityDialog.jsonPage.description')}
+
+
+
>
);
}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.test.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.test.tsx
new file mode 100644
index 0000000000..077e3c1be5
--- /dev/null
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.test.tsx
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2026 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { renderInTestApp } from '@backstage/test-utils';
+import { screen } from '@testing-library/react';
+import { entityRouteRef } from '../../../routes';
+import { OverviewPage } from './OverviewPage';
+
+const mountedRoutes = {
+ '/catalog/:namespace/:kind/:name/*': entityRouteRef,
+};
+
+const entity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ namespace: 'default',
+ name: 'test-component',
+ uid: 'test-uid-123',
+ etag: 'test-etag-456',
+ annotations: {
+ 'backstage.io/source-location': 'url:https://github.com/example/repo',
+ 'backstage.io/techdocs-ref': 'dir:.',
+ },
+ labels: {
+ 'backstage.io/custom': 'value',
+ },
+ tags: ['java', 'data'],
+ },
+ spec: {
+ type: 'service',
+ lifecycle: 'production',
+ owner: 'team-a',
+ },
+ relations: [
+ { type: 'ownedBy', targetRef: 'group:default/team-a' },
+ { type: 'ownedBy', targetRef: 'group:default/team-b' },
+ { type: 'dependsOn', targetRef: 'component:default/other' },
+ ],
+} as any;
+
+describe('OverviewPage', () => {
+ it('renders identity key-value pairs', async () => {
+ await renderInTestApp(, { mountedRoutes });
+
+ const terms = screen.getAllByRole('term');
+ const definitions = screen.getAllByRole('definition');
+ const termTexts = terms.map(el => el.textContent);
+
+ expect(termTexts).toContain('apiVersion');
+ expect(termTexts).toContain('kind');
+ expect(termTexts).toContain('uid');
+ expect(termTexts).toContain('etag');
+ expect(termTexts).toContain('entityRef');
+
+ const defTexts = definitions.map(el => el.textContent);
+ expect(defTexts).toContain('backstage.io/v1alpha1');
+ expect(defTexts).toContain('Component');
+ expect(defTexts).toContain('test-uid-123');
+ expect(defTexts).toContain('test-etag-456');
+ expect(defTexts).toContain('component:default/test-component');
+ });
+
+ it('renders annotation values as links when they start with https:// or url:https://', async () => {
+ await renderInTestApp(, { mountedRoutes });
+
+ // url:https:// prefix is stripped from href but full value shown as link text
+ // The accessible name includes ", Opens in a new window" appended by the Link component
+ const sourceLocationLink = await screen.findByRole('link', {
+ name: /url:https:\/\/github\.com\/example\/repo/,
+ });
+ expect(sourceLocationLink).toHaveAttribute(
+ 'href',
+ 'https://github.com/example/repo',
+ );
+
+ // Plain non-URL annotation value renders as text, not a link
+ expect(
+ screen.queryByRole('link', { name: 'dir:.' }),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText('dir:.')).toBeInTheDocument();
+ });
+
+ it('renders tags', async () => {
+ await renderInTestApp(, { mountedRoutes });
+
+ expect(await screen.findByText('java')).toBeInTheDocument();
+ expect(screen.getByText('data')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx
index 4feabf9c38..c7ff12a486 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx
@@ -15,36 +15,228 @@
*/
import { AlphaEntity } from '@backstage/catalog-model/alpha';
-import Box from '@material-ui/core/Box';
-import DialogContentText from '@material-ui/core/DialogContentText';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
-import Typography from '@material-ui/core/Typography';
+import { Link } from '@backstage/core-components';
+import {
+ Text,
+ Box,
+ Flex,
+ Card,
+ CardHeader,
+ CardBody,
+ TagGroup,
+ Tag,
+ ButtonIcon,
+ ButtonLink,
+ VisuallyHidden,
+} from '@backstage/ui';
import { makeStyles } from '@material-ui/core/styles';
+import classNames from 'classnames';
+import { RiFileCopyLine, RiCheckLine, RiQuestionLine } from '@remixicon/react';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
+import { ReactNode, useEffect, useRef, useState } from 'react';
import { EntityRefLink } from '../../EntityRefLink';
-import {
- Container,
- HelpIcon,
- KeyValueListItem,
- ListItemText,
- ListSubheader,
-} from './common';
+import { ListSection, ListItemRow } from './common';
import { stringifyEntityRef } from '@backstage/catalog-model';
-import { CopyTextButton } from '@backstage/core-components';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
const useStyles = makeStyles({
- root: {
+ headingWithIcon: {
display: 'flex',
- flexDirection: 'column',
+ alignItems: 'center',
+ },
+ definitionList: {
+ margin: 0,
+ padding: 0,
+ },
+ definitionItem: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ marginTop: 'var(--bui-space-4)',
+ paddingLeft: 'var(--bui-space-4)',
+ fontFamily: 'monospace',
+ fontSize: 'var(--bui-font-size-3)',
+ '&:first-child': {
+ marginTop: 0,
+ },
+ },
+ definitionContent: {
+ flex: 1,
+ minWidth: 0,
+ },
+ definitionKey: {
+ fontWeight: 'bold',
+ },
+ definitionValue: {
+ margin: 0,
+ marginTop: 'var(--bui-space-1)',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ },
+ copyAction: {
+ marginLeft: 'var(--bui-space-2)',
+ flexShrink: 0,
+ },
+ relationGroup: {
+ '& + &': {
+ marginTop: 'var(--bui-space-4)',
+ },
+ },
+ monospace: {
+ fontFamily: 'monospace',
+ },
+ sectionHeading: {
+ marginTop: 'var(--bui-space-3)',
+ },
+ metadataList: {
+ marginTop: 'var(--bui-space-2)',
+ },
+ relationList: {
+ marginTop: 'var(--bui-space-2)',
+ },
+ tagGroup: {
+ marginTop: 'var(--bui-space-3)',
+ paddingLeft: 'var(--bui-space-4)',
},
});
+// Extracts a link from a value, if possible
+function findLink(value: string): string | undefined {
+ if (value.match(/^url:https?:\/\//)) {
+ return value.slice('url:'.length);
+ }
+ if (value.match(/^https?:\/\//)) {
+ return value;
+ }
+ return undefined;
+}
+
+function entriesToItems(entries: [string, string][]) {
+ return entries.map(([key, value]) => {
+ const link = findLink(value);
+ return {
+ key,
+ value: link ? {value} : value,
+ };
+ });
+}
+
+function CopyButton({ text, label }: { text: string; label: string }) {
+ const { t } = useTranslationRef(catalogReactTranslationRef);
+ const [copied, setCopied] = useState(false);
+ const timerRef = useRef>();
+
+ useEffect(() => () => clearTimeout(timerRef.current), []);
+
+ const handlePress = async () => {
+ try {
+ await window.navigator.clipboard.writeText(text);
+ setCopied(true);
+ timerRef.current = setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Clipboard access denied or unavailable
+ }
+ };
+
+ return (
+ <>
+ : }
+ aria-label={t('inspectEntityDialog.overviewPage.copyAriaLabel', {
+ label,
+ })}
+ variant="tertiary"
+ size="small"
+ onPress={handlePress}
+ />
+
+ {copied ? t('inspectEntityDialog.overviewPage.copiedStatus') : ''}
+
+ >
+ );
+}
+
+function HelpIcon(props: { to: string }) {
+ const { t } = useTranslationRef(catalogReactTranslationRef);
+ return (
+ }
+ aria-label={t('inspectEntityDialog.overviewPage.helpLinkAriaLabel')}
+ />
+ );
+}
+
+function Container(props: {
+ title: ReactNode;
+ helpLink?: string;
+ children: ReactNode;
+}) {
+ const classes = useStyles();
+ return (
+
+
+
+ {props.title}
+ {props.helpLink && }
+
+
+ {props.children}
+
+ );
+}
+
+function ListSubheader(props: { children: ReactNode; className?: string }) {
+ const classes = useStyles();
+ return (
+
+ {props.children}
+
+ );
+}
+
+function KeyValueList(props: {
+ items: { key: string; value: ReactNode; copyable?: boolean }[];
+ className?: string;
+ 'aria-label'?: string;
+}) {
+ const classes = useStyles();
+ return (
+
+ {props.items.map(item => (
+
+
+
- {item.key}
+ - {item.value}
+
+ {item.copyable && typeof item.value === 'string' && (
+
+
+
+ )}
+
+ ))}
+
+ );
+}
+
export function OverviewPage(props: { entity: AlphaEntity }) {
const classes = useStyles();
const {
@@ -64,142 +256,109 @@ export function OverviewPage(props: { entity: AlphaEntity }) {
const entityRef = stringifyEntityRef(props.entity);
return (
- <>
-
- {t('inspectEntityDialog.overviewPage.title')}
-
-
-
-
-
-
-
-
-
-
- {spec?.type && (
-
-
-
- )}
- {metadata.uid && (
-
-
-
-
-
-
- )}
- {metadata.etag && (
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
+
+
+
+
-
- {!!Object.keys(metadata.annotations || {}).length && (
-
- {t('inspectEntityDialog.overviewPage.annotations')}
-
-
- }
- >
- {Object.entries(metadata.annotations!).map(entry => (
-
- ))}
-
- )}
- {!!Object.keys(metadata.labels || {}).length && (
-
- {t('inspectEntityDialog.overviewPage.labels')}
-
- }
- >
- {Object.entries(metadata.labels!).map(entry => (
-
- ))}
-
- )}
- {!!metadata.tags?.length && (
-
- {t('inspectEntityDialog.overviewPage.tags')}
-
- }
- >
- {metadata.tags.map((tag, index) => (
-
-
-
-
- ))}
-
- )}
-
-
- {!!relations.length && (
-
- {Object.entries(groupedRelations).map(
- ([type, groupRelations], index) => (
-
- {type}}>
- {groupRelations.map(group => (
-
-
- }
- />
-
- ))}
-
-
- ),
- )}
-
+
+ {!!Object.keys(metadata.annotations || {}).length && (
+ <>
+
+ {t('inspectEntityDialog.overviewPage.annotations')}
+
+
+
+ >
)}
-
- {!!status.items?.length && (
-
- {status.items.map((item, index) => (
-
-
- {item.level}: {item.type}
-
- {item.message}
-
- ))}
-
+ {!!Object.keys(metadata.labels || {}).length && (
+ <>
+
+ {t('inspectEntityDialog.overviewPage.labels')}
+
+
+ >
)}
-
- >
+ {!!metadata.tags?.length && (
+ <>
+
+ {t('inspectEntityDialog.overviewPage.tags')}
+
+
+ {metadata.tags.map(tag => (
+
+ {tag}
+
+ ))}
+
+ >
+ )}
+
+
+ {!!relations.length && (
+
+ {Object.entries(groupedRelations).map(([type, groupRelations]) => (
+
+
+ {type}
+
+
+ {groupRelations.map(group => (
+
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ {!!status.items?.length && (
+
+ {status.items.map((item, index) => (
+
+
+ {item.level}: {item.type}
+
+ {item.message}
+
+ ))}
+
+ )}
+
);
}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx
index 6763707810..e04fa3ae0b 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { CodeSnippet } from '@backstage/core-components';
-import DialogContentText from '@material-ui/core/DialogContentText';
+import { Text } from '@backstage/ui';
import YAML from 'yaml';
import { sortKeys } from './util';
import { catalogReactTranslationRef } from '../../../translation';
@@ -26,21 +26,14 @@ export function YamlPage(props: { entity: Entity }) {
const { t } = useTranslationRef(catalogReactTranslationRef);
return (
<>
-
- {t('inspectEntityDialog.yamlPage.title')}
-
-
- {t('inspectEntityDialog.yamlPage.description')}
-
-
-
-
-
-
+ {t('inspectEntityDialog.yamlPage.description')}
+
+
+
>
);
}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx
index ff8a66e10d..b335ff8e9f 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx
@@ -14,113 +14,57 @@
* limitations under the License.
*/
-import { Link } from '@backstage/core-components';
-import Box from '@material-ui/core/Box';
-import Card from '@material-ui/core/Card';
-import CardContent from '@material-ui/core/CardContent';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import MuiListItemText from '@material-ui/core/ListItemText';
-import MuiListSubheader from '@material-ui/core/ListSubheader';
-import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
-import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
+import classNames from 'classnames';
import { ReactNode } from 'react';
-const useStyles = makeStyles(theme => ({
- root: {
+const useStyles = makeStyles({
+ list: {
+ listStyle: 'none',
+ margin: 0,
+ padding: 0,
+ },
+ indented: {
+ paddingLeft: 'var(--bui-space-4)',
+ },
+ listItem: {
display: 'flex',
- flexDirection: 'column',
- },
- marginTop: {
- marginTop: theme.spacing(2),
- },
- helpIcon: {
- marginLeft: theme.spacing(1),
- color: theme.palette.text.disabled,
- },
- monospace: {
+ alignItems: 'flex-start',
+ marginTop: 'var(--bui-space-1)',
+ paddingLeft: 'var(--bui-space-4)',
fontFamily: 'monospace',
+ fontSize: 'var(--bui-font-size-3)',
+ '&:first-child': {
+ marginTop: 0,
+ },
},
-}));
+});
-export function ListItemText(props: {
- primary: ReactNode;
- secondary?: ReactNode;
-}) {
- const classes = useStyles();
- return (
-
- );
-}
-
-export function ListSubheader(props: { children?: ReactNode }) {
- const classes = useStyles();
- return (
-
- {props.children}
-
- );
-}
-
-export function Container(props: {
- title: ReactNode;
- helpLink?: string;
+export function ListSection(props: {
children: ReactNode;
-}) {
- return (
-
-
-
-
- {props.title}
- {props.helpLink && }
-
- {props.children}
-
-
-
- );
-}
-
-// Extracts a link from a value, if possible
-function findLink(value: string): string | undefined {
- if (value.match(/^url:https?:\/\//)) {
- return value.slice('url:'.length);
- }
- if (value.match(/^https?:\/\//)) {
- return value;
- }
- return undefined;
-}
-
-export function KeyValueListItem(props: {
indent?: boolean;
- entry: [string, string];
+ className?: string;
+ 'aria-label'?: string;
}) {
- const [key, value] = props.entry;
- const link = findLink(value);
-
- return (
-
- {props.indent && }
- {value} : value}
- />
-
- );
-}
-
-export function HelpIcon(props: { to: string }) {
const classes = useStyles();
return (
-
-
-
+
);
}
+
+/**
+ * A dense monospace list item.
+ */
+export function ListItemRow(props: { children: ReactNode }) {
+ const classes = useStyles();
+ return {props.children};
+}
diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts
index db0c796424..b9a43eb1cf 100644
--- a/plugins/catalog-react/src/translation.ts
+++ b/plugins/catalog-react/src/translation.ts
@@ -82,6 +82,7 @@ export const catalogReactTranslationRef = createTranslationRef({
alertNoEntity: 'There were no other entities on this location.',
locationHeader: 'At the same location',
originHeader: 'At the same origin',
+ entityListAriaLabel: 'Colocated entities',
},
jsonPage: {
title: 'Entity as JSON',
@@ -105,6 +106,9 @@ export const catalogReactTranslationRef = createTranslationRef({
annotations: 'Annotations',
labels: 'Labels',
tags: 'Tags',
+ copyAriaLabel: 'Copy {{label}}',
+ copiedStatus: 'Copied',
+ helpLinkAriaLabel: 'Learn more',
},
yamlPage: {
title: 'Entity as YAML',
diff --git a/yarn.lock b/yarn.lock
index 23863cdf4e..f1edc76f1d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5342,6 +5342,7 @@ __metadata:
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@react-hookz/web": "npm:^24.0.0"
+ "@remixicon/react": "npm:^4.6.0"
"@testing-library/dom": "npm:^10.0.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"