update NFS header migration scope
Roll back the TechDocs, catalog graph, and catalog import migrations for now while keeping the shared sub-page routing fix and the page-level header cleanup for search, notifications, and catalog entity pages. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -2,13 +2,10 @@
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-auth': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-graph': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-unprocessed-entities': patch
|
||||
'@backstage/plugin-devtools': patch
|
||||
'@backstage/plugin-notifications': patch
|
||||
'@backstage/plugin-search': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
createExtensionBlueprint,
|
||||
createExtensionInput,
|
||||
} from '../wiring';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { waitFor, screen } from '@testing-library/react';
|
||||
|
||||
describe('PageBlueprint', () => {
|
||||
const mockRouteRef = createRouteRef();
|
||||
@@ -279,4 +279,50 @@ describe('PageBlueprint', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should resolve sub-page tab hrefs relative to the parent page', async () => {
|
||||
const myPage = PageBlueprint.make({
|
||||
name: 'test-page',
|
||||
params: {
|
||||
path: '/test',
|
||||
routeRef: mockRouteRef,
|
||||
title: 'Test',
|
||||
},
|
||||
});
|
||||
|
||||
const SubPageBlueprint = createExtensionBlueprint({
|
||||
kind: 'sub-page',
|
||||
attachTo: { id: 'page:test-page', input: 'pages' },
|
||||
output: [
|
||||
coreExtensionData.routePath,
|
||||
coreExtensionData.reactElement,
|
||||
coreExtensionData.title.optional(),
|
||||
],
|
||||
factory() {
|
||||
return [
|
||||
coreExtensionData.routePath('config'),
|
||||
coreExtensionData.title('Config'),
|
||||
coreExtensionData.reactElement(<div>Config page</div>),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(myPage).add(
|
||||
SubPageBlueprint.make({ name: 'config', params: {} }),
|
||||
);
|
||||
|
||||
renderInTestApp(tester.reactElement(), {
|
||||
mountedRoutes: {
|
||||
'/test/*': mockRouteRef,
|
||||
},
|
||||
initialRouteEntries: ['/test'],
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tab', { name: 'Config' })).toHaveAttribute(
|
||||
'href',
|
||||
'/test/config',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { JSX } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Routes, Route, Navigate, useResolvedPath } from 'react-router-dom';
|
||||
import { IconElement } from '../icons/types';
|
||||
import { RouteRef } from '../routing';
|
||||
import {
|
||||
@@ -72,6 +72,7 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
{ config, node, inputs },
|
||||
) {
|
||||
const title = config.title ?? params.title;
|
||||
const routePath = config.path ?? params.path;
|
||||
const icon = params.icon;
|
||||
const pluginId = node.spec.plugin.pluginId;
|
||||
const noHeader = params.noHeader ?? false;
|
||||
@@ -79,7 +80,7 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId;
|
||||
const resolvedIcon = icon ?? node.spec.plugin.icon;
|
||||
|
||||
yield coreExtensionData.routePath(config.path ?? params.path);
|
||||
yield coreExtensionData.routePath(routePath);
|
||||
if (params.loader) {
|
||||
const loader = params.loader;
|
||||
const PageContent = () => {
|
||||
@@ -99,24 +100,34 @@ export const PageBlueprint = createExtensionBlueprint({
|
||||
};
|
||||
yield coreExtensionData.reactElement(<PageContent />);
|
||||
} else if (inputs.pages.length > 0) {
|
||||
// Parent page with sub-pages - render header with tabs
|
||||
const tabs: PageLayoutTab[] = inputs.pages.map(page => {
|
||||
const path = page.get(coreExtensionData.routePath);
|
||||
const tabTitle = page.get(coreExtensionData.title);
|
||||
const tabIcon = page.get(coreExtensionData.icon);
|
||||
return {
|
||||
id: path,
|
||||
label: tabTitle || path,
|
||||
icon: tabIcon,
|
||||
href: path,
|
||||
};
|
||||
});
|
||||
|
||||
const PageContent = () => {
|
||||
const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath);
|
||||
|
||||
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
|
||||
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
|
||||
const parentPath = useResolvedPath('.').pathname.replace(/\/$/, '');
|
||||
const staticParentPath =
|
||||
routePath.startsWith('/') &&
|
||||
!routePath.includes('/:') &&
|
||||
!routePath.includes('*')
|
||||
? routePath.replace(/\/$/, '')
|
||||
: undefined;
|
||||
const tabs: PageLayoutTab[] = inputs.pages.map(page => {
|
||||
const path = page.get(coreExtensionData.routePath);
|
||||
const tabTitle = page.get(coreExtensionData.title);
|
||||
const tabIcon = page.get(coreExtensionData.icon);
|
||||
const tabPath = path.replace(/^\/+/, '');
|
||||
const basePath = staticParentPath ?? parentPath ?? '';
|
||||
const href = path.startsWith('/')
|
||||
? path
|
||||
: `${basePath}/${tabPath}`.replace(/\/{2,}/g, '/');
|
||||
|
||||
return {
|
||||
id: path,
|
||||
label: tabTitle || path,
|
||||
icon: tabIcon,
|
||||
href,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
|
||||
@@ -79,8 +79,8 @@ const CatalogGraphPage = PageBlueprint.makeWithOverrides({
|
||||
path: '/catalog-graph',
|
||||
routeRef: catalogGraphRouteRef,
|
||||
loader: () =>
|
||||
import('./components/CatalogGraphPage/CatalogGraphPage').then(m => (
|
||||
<m.NfsCatalogGraphPage {...config} />
|
||||
import('./components/CatalogGraphPage').then(m => (
|
||||
<m.CatalogGraphPage {...config} />
|
||||
)),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import {
|
||||
entityRouteRef,
|
||||
humanizeEntityRef,
|
||||
@@ -117,23 +116,21 @@ const useStyles = makeStyles(
|
||||
{ name: 'PluginCatalogGraphCatalogGraphPage' },
|
||||
);
|
||||
|
||||
type CatalogGraphPageProps = {
|
||||
initialState?: {
|
||||
selectedRelations?: string[];
|
||||
selectedKinds?: string[];
|
||||
rootEntityRefs?: string[];
|
||||
maxDepth?: number;
|
||||
unidirectional?: boolean;
|
||||
mergeRelations?: boolean;
|
||||
direction?: Direction;
|
||||
showFilters?: boolean;
|
||||
curve?: 'curveStepBefore' | 'curveMonotoneX';
|
||||
};
|
||||
} & Partial<EntityRelationsGraphProps>;
|
||||
|
||||
function CatalogGraphPageContent(
|
||||
props: CatalogGraphPageProps & { headerVariant: 'legacy' | 'bui' },
|
||||
) {
|
||||
export const CatalogGraphPage = (
|
||||
props: {
|
||||
initialState?: {
|
||||
selectedRelations?: string[];
|
||||
selectedKinds?: string[];
|
||||
rootEntityRefs?: string[];
|
||||
maxDepth?: number;
|
||||
unidirectional?: boolean;
|
||||
mergeRelations?: boolean;
|
||||
direction?: Direction;
|
||||
showFilters?: boolean;
|
||||
curve?: 'curveStepBefore' | 'curveMonotoneX';
|
||||
};
|
||||
} & Partial<EntityRelationsGraphProps>,
|
||||
) => {
|
||||
const { relationPairs, initialState, entityFilter } = props;
|
||||
const { t } = useTranslationRef(catalogGraphTranslationRef);
|
||||
const navigate = useNavigate();
|
||||
@@ -188,124 +185,93 @@ function CatalogGraphPageContent(
|
||||
[catalogEntityRoute, navigate, setRootEntityNames, analytics],
|
||||
);
|
||||
|
||||
const pageBody = (
|
||||
<Grid container alignItems="stretch" className={classes.container}>
|
||||
{showFilters && (
|
||||
<Grid item xs={12} lg={2} className={classes.filters}>
|
||||
<MaxDepthFilter value={maxDepth} onChange={setMaxDepth} />
|
||||
<SelectedKindsFilter
|
||||
value={selectedKinds}
|
||||
onChange={setSelectedKinds}
|
||||
/>
|
||||
<SelectedRelationsFilter
|
||||
value={selectedRelations}
|
||||
onChange={setSelectedRelations}
|
||||
/>
|
||||
<DirectionFilter value={direction} onChange={setDirection} />
|
||||
<CurveFilter value={curve} onChange={setCurve} />
|
||||
<SwitchFilter
|
||||
value={unidirectional}
|
||||
onChange={setUnidirectional}
|
||||
label={t('catalogGraphPage.simplifiedSwitchLabel')}
|
||||
/>
|
||||
<SwitchFilter
|
||||
value={mergeRelations}
|
||||
onChange={setMergeRelations}
|
||||
label={t('catalogGraphPage.mergeRelationsSwitchLabel')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs className={classes.fullHeight}>
|
||||
<Paper className={classes.graphWrapper}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
display="block"
|
||||
className={classes.legend}
|
||||
>
|
||||
<ZoomOutMap className="icon" />{' '}
|
||||
{t('catalogGraphPage.zoomOutDescription')}
|
||||
</Typography>
|
||||
<EntityRelationsGraph
|
||||
{...props}
|
||||
rootEntityNames={rootEntityNames}
|
||||
maxDepth={maxDepth}
|
||||
kinds={
|
||||
selectedKinds && selectedKinds.length > 0
|
||||
? selectedKinds
|
||||
: undefined
|
||||
}
|
||||
relations={
|
||||
selectedRelations && selectedRelations.length > 0
|
||||
? selectedRelations
|
||||
: undefined
|
||||
}
|
||||
mergeRelations={mergeRelations}
|
||||
unidirectional={unidirectional}
|
||||
onNodeClick={onNodeClick}
|
||||
direction={direction}
|
||||
relationPairs={relationPairs}
|
||||
entityFilter={entityFilter}
|
||||
className={classes.graph}
|
||||
zoom="enabled"
|
||||
curve={curve}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
const filterAction = (
|
||||
<ToggleButton
|
||||
value="show filters"
|
||||
selected={showFilters}
|
||||
onChange={() => toggleShowFilters()}
|
||||
>
|
||||
<FilterListIcon /> {t('catalogGraphPage.filterToggleButtonTitle')}
|
||||
</ToggleButton>
|
||||
);
|
||||
const headerActions = (
|
||||
<>
|
||||
{filterAction}
|
||||
<SupportButton>
|
||||
{t('catalogGraphPage.supportButtonDescription')}
|
||||
</SupportButton>
|
||||
</>
|
||||
);
|
||||
const subtitle = rootEntityNames.map(e => humanizeEntityRef(e)).join(', ');
|
||||
|
||||
if (props.headerVariant === 'bui') {
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={subtitle || t('catalogGraphPage.title')}
|
||||
customActions={headerActions}
|
||||
/>
|
||||
<Content stretch className={classes.content}>
|
||||
{pageBody}
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title={t('catalogGraphPage.title')} subtitle={subtitle} />
|
||||
<Header
|
||||
title={t('catalogGraphPage.title')}
|
||||
subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')}
|
||||
/>
|
||||
<Content stretch className={classes.content}>
|
||||
<ContentHeader titleComponent={filterAction}>
|
||||
<ContentHeader
|
||||
titleComponent={
|
||||
<ToggleButton
|
||||
value="show filters"
|
||||
selected={showFilters}
|
||||
onChange={() => toggleShowFilters()}
|
||||
>
|
||||
<FilterListIcon /> {t('catalogGraphPage.filterToggleButtonTitle')}
|
||||
</ToggleButton>
|
||||
}
|
||||
>
|
||||
<SupportButton>
|
||||
{t('catalogGraphPage.supportButtonDescription')}
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
{pageBody}
|
||||
<Grid container alignItems="stretch" className={classes.container}>
|
||||
{showFilters && (
|
||||
<Grid item xs={12} lg={2} className={classes.filters}>
|
||||
<MaxDepthFilter value={maxDepth} onChange={setMaxDepth} />
|
||||
<SelectedKindsFilter
|
||||
value={selectedKinds}
|
||||
onChange={setSelectedKinds}
|
||||
/>
|
||||
<SelectedRelationsFilter
|
||||
value={selectedRelations}
|
||||
onChange={setSelectedRelations}
|
||||
/>
|
||||
<DirectionFilter value={direction} onChange={setDirection} />
|
||||
<CurveFilter value={curve} onChange={setCurve} />
|
||||
<SwitchFilter
|
||||
value={unidirectional}
|
||||
onChange={setUnidirectional}
|
||||
label={t('catalogGraphPage.simplifiedSwitchLabel')}
|
||||
/>
|
||||
<SwitchFilter
|
||||
value={mergeRelations}
|
||||
onChange={setMergeRelations}
|
||||
label={t('catalogGraphPage.mergeRelationsSwitchLabel')}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs className={classes.fullHeight}>
|
||||
<Paper className={classes.graphWrapper}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
display="block"
|
||||
className={classes.legend}
|
||||
>
|
||||
<ZoomOutMap className="icon" />{' '}
|
||||
{t('catalogGraphPage.zoomOutDescription')}
|
||||
</Typography>
|
||||
<EntityRelationsGraph
|
||||
{...props}
|
||||
rootEntityNames={rootEntityNames}
|
||||
maxDepth={maxDepth}
|
||||
kinds={
|
||||
selectedKinds && selectedKinds.length > 0
|
||||
? selectedKinds
|
||||
: undefined
|
||||
}
|
||||
relations={
|
||||
selectedRelations && selectedRelations.length > 0
|
||||
? selectedRelations
|
||||
: undefined
|
||||
}
|
||||
mergeRelations={mergeRelations}
|
||||
unidirectional={unidirectional}
|
||||
onNodeClick={onNodeClick}
|
||||
direction={direction}
|
||||
relationPairs={relationPairs}
|
||||
entityFilter={entityFilter}
|
||||
className={classes.graph}
|
||||
zoom="enabled"
|
||||
curve={curve}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export const CatalogGraphPage = (props: CatalogGraphPageProps) => (
|
||||
<CatalogGraphPageContent {...props} headerVariant="legacy" />
|
||||
);
|
||||
|
||||
export const NfsCatalogGraphPage = (props: CatalogGraphPageProps) => (
|
||||
<CatalogGraphPageContent {...props} headerVariant="bui" />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"@backstage/plugin-catalog-common": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/plugin-permission-react": "workspace:^",
|
||||
"@backstage/ui": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
|
||||
@@ -43,9 +43,9 @@ const catalogImportPage = PageBlueprint.make({
|
||||
path: '/catalog-import',
|
||||
routeRef: rootRouteRef,
|
||||
loader: () =>
|
||||
import('./components/ImportPage/ImportPage').then(m => (
|
||||
import('./components/ImportPage').then(m => (
|
||||
<RequirePermission permission={catalogEntityCreatePermission}>
|
||||
<m.NfsImportPage />
|
||||
<m.ImportPage />
|
||||
</RequirePermission>
|
||||
)),
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from '@backstage/core-components';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
@@ -53,22 +52,17 @@ export const DefaultImportPage = () => {
|
||||
<ImportStepper />
|
||||
</Grid>,
|
||||
];
|
||||
const headerTitle = t('defaultImportPage.headerTitle');
|
||||
const supportAction = (
|
||||
<SupportButton>
|
||||
{t('defaultImportPage.supportTitle', { appTitle })}
|
||||
</SupportButton>
|
||||
);
|
||||
const contentHeaderTitle = t('defaultImportPage.contentHeaderTitle', {
|
||||
appTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title={headerTitle} />
|
||||
<Header title={t('defaultImportPage.headerTitle')} />
|
||||
<Content>
|
||||
<ContentHeader title={contentHeaderTitle}>
|
||||
{supportAction}
|
||||
<ContentHeader
|
||||
title={t('defaultImportPage.contentHeaderTitle', { appTitle })}
|
||||
>
|
||||
<SupportButton>
|
||||
{t('defaultImportPage.supportTitle', { appTitle })}
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
@@ -78,39 +72,3 @@ export const DefaultImportPage = () => {
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const NfsDefaultImportPage = () => {
|
||||
const { t } = useTranslationRef(catalogImportTranslationRef);
|
||||
const theme = useTheme();
|
||||
const configApi = useApi(configApiRef);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const appTitle = configApi.getOptionalString('app.title') || 'Backstage';
|
||||
|
||||
const contentItems = [
|
||||
<Grid key={0} item xs={12} md={4} lg={6} xl={8}>
|
||||
<ImportInfoCard />
|
||||
</Grid>,
|
||||
|
||||
<Grid key={1} item xs={12} md={8} lg={6} xl={4}>
|
||||
<ImportStepper />
|
||||
</Grid>,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={t('defaultImportPage.contentHeaderTitle', { appTitle })}
|
||||
customActions={
|
||||
<SupportButton>
|
||||
{t('defaultImportPage.supportTitle', { appTitle })}
|
||||
</SupportButton>
|
||||
}
|
||||
/>
|
||||
<Content>
|
||||
<Grid container spacing={2}>
|
||||
{isMobile ? contentItems : contentItems.reverse()}
|
||||
</Grid>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { DefaultImportPage } from '../DefaultImportPage';
|
||||
import { NfsDefaultImportPage } from '../DefaultImportPage/DefaultImportPage';
|
||||
|
||||
/**
|
||||
* The whole catalog import page.
|
||||
@@ -28,9 +27,3 @@ export const ImportPage = () => {
|
||||
|
||||
return outlet || <DefaultImportPage />;
|
||||
};
|
||||
|
||||
export const NfsImportPage = () => {
|
||||
const outlet = useOutlet();
|
||||
|
||||
return outlet || <NfsDefaultImportPage />;
|
||||
};
|
||||
|
||||
@@ -27,14 +27,13 @@ import useAsync from 'react-use/esm/useAsync';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Box from '@material-ui/core/Box';
|
||||
|
||||
import { Breadcrumbs } from '@backstage/core-components';
|
||||
import { Header, Breadcrumbs } from '@backstage/core-components';
|
||||
import {
|
||||
useApi,
|
||||
useRouteRef,
|
||||
useRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/frontend-plugin-api';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
|
||||
import {
|
||||
Entity,
|
||||
@@ -49,6 +48,7 @@ import {
|
||||
EntityRefLink,
|
||||
InspectEntityDialog,
|
||||
UnregisterEntityDialog,
|
||||
EntityDisplayName,
|
||||
FavoriteEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
@@ -57,11 +57,12 @@ import { EntityContextMenu } from '../../../components/EntityContextMenu';
|
||||
import { rootRouteRef, unregisterRedirectRouteRef } from '../../../routes';
|
||||
|
||||
function headerProps(
|
||||
_paramKind: string | undefined,
|
||||
paramKind: string | undefined,
|
||||
paramNamespace: string | undefined,
|
||||
paramName: string | undefined,
|
||||
entity: Entity | undefined,
|
||||
): { headerTitle: string } {
|
||||
): { headerTitle: string; headerType: string } {
|
||||
const kind = paramKind ?? entity?.kind ?? '';
|
||||
const namespace = paramNamespace ?? entity?.metadata.namespace ?? '';
|
||||
const name =
|
||||
entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? '';
|
||||
@@ -70,6 +71,14 @@ function headerProps(
|
||||
headerTitle: `${name}${
|
||||
namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : ''
|
||||
}`,
|
||||
headerType: (() => {
|
||||
let t = kind.toLocaleLowerCase('en-US');
|
||||
if (entity && entity.spec && 'type' in entity.spec) {
|
||||
t += ' — ';
|
||||
t += (entity.spec as { type: string }).type.toLocaleLowerCase('en-US');
|
||||
}
|
||||
return t;
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,12 +112,30 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function EntityHeaderTitle() {
|
||||
const { entity } = useAsyncEntity();
|
||||
const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
|
||||
const { headerTitle: title } = headerProps(kind, namespace, name, entity);
|
||||
return (
|
||||
<Box display="inline-flex" alignItems="center" height="1em" maxWidth="100%">
|
||||
<Box
|
||||
component="span"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
>
|
||||
{entity ? <EntityDisplayName entityRef={entity} hideIcon /> : title}
|
||||
</Box>
|
||||
{entity && <FavoriteEntity entity={entity} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function EntityHeaderSubtitle(props: { parentEntityRelations?: string[] }) {
|
||||
const { parentEntityRelations } = props;
|
||||
const classes = useStyles();
|
||||
const { entity } = useAsyncEntity();
|
||||
const { name } = useRouteRefParams(entityRouteRef);
|
||||
const entityName = entity?.metadata.title ?? name;
|
||||
const parentEntity = findParentRelation(
|
||||
entity?.relations ?? [],
|
||||
parentEntityRelations ?? [],
|
||||
@@ -132,7 +159,7 @@ function EntityHeaderSubtitle(props: { parentEntityRelations?: string[] }) {
|
||||
<EntityRefLink entityRef={ancestorEntity.targetRef} disableTooltip />
|
||||
)}
|
||||
<EntityRefLink entityRef={parentEntity.targetRef} disableTooltip />
|
||||
{entityName}
|
||||
{name}
|
||||
</Breadcrumbs>
|
||||
) : null;
|
||||
}
|
||||
@@ -176,7 +203,12 @@ export function EntityHeader(props: {
|
||||
} = props;
|
||||
const { entity } = useAsyncEntity();
|
||||
const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
|
||||
const { headerTitle } = headerProps(kind, namespace, name, entity);
|
||||
const { headerTitle: entityFallbackText, headerType: type } = headerProps(
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
entity,
|
||||
);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -233,44 +265,28 @@ export function EntityHeader(props: {
|
||||
);
|
||||
|
||||
const inspectDialogOpen = typeof selectedInspectEntityDialogTab === 'string';
|
||||
const customTitle = typeof title === 'string' ? undefined : title;
|
||||
const headerSubtitle = subtitle ?? (
|
||||
<EntityHeaderSubtitle parentEntityRelations={parentEntityRelations} />
|
||||
);
|
||||
const renderedTitle = typeof title === 'string' ? title : headerTitle;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderPage
|
||||
title={renderedTitle}
|
||||
customActions={
|
||||
entity ? (
|
||||
<>
|
||||
<FavoriteEntity entity={entity} />
|
||||
<EntityContextMenu
|
||||
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
|
||||
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
|
||||
contextMenuItems={contextMenuItems}
|
||||
onInspectEntity={openInspectEntityDialog}
|
||||
onUnregisterEntity={openUnregisterEntityDialog}
|
||||
/>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{(customTitle || headerSubtitle || entity) && (
|
||||
<Box mt={2}>
|
||||
{customTitle}
|
||||
{headerSubtitle}
|
||||
{entity && (
|
||||
<Box mt={customTitle || headerSubtitle ? 1 : 0}>
|
||||
<EntityLabels entity={entity} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Header
|
||||
pageTitleOverride={entityFallbackText}
|
||||
type={type}
|
||||
title={title ?? <EntityHeaderTitle />}
|
||||
subtitle={
|
||||
subtitle ?? (
|
||||
<EntityHeaderSubtitle parentEntityRelations={parentEntityRelations} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{entity && (
|
||||
<>
|
||||
<EntityLabels entity={entity} />
|
||||
<EntityContextMenu
|
||||
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
|
||||
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
|
||||
contextMenuItems={contextMenuItems}
|
||||
onInspectEntity={openInspectEntityDialog}
|
||||
onUnregisterEntity={openUnregisterEntityDialog}
|
||||
/>
|
||||
<InspectEntityDialog
|
||||
entity={entity!}
|
||||
initialTab={
|
||||
@@ -290,6 +306,6 @@ export function EntityHeader(props: {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
PageWithHeader,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { ConfirmProvider } from 'material-ui-confirm';
|
||||
import { useSignal } from '@backstage/plugin-signals-react';
|
||||
@@ -233,12 +232,7 @@ function NotificationsPageContent(
|
||||
);
|
||||
|
||||
if (headerVariant === 'bui') {
|
||||
return (
|
||||
<>
|
||||
<HeaderPage title={subtitle || title} />
|
||||
{pageContent}
|
||||
</>
|
||||
);
|
||||
return pageContent;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+98
-102
@@ -25,7 +25,6 @@ import {
|
||||
DocsIcon,
|
||||
useSidebarPinState,
|
||||
} from '@backstage/core-components';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import {
|
||||
useApi,
|
||||
discoveryApiRef,
|
||||
@@ -142,113 +141,110 @@ export const searchPage = PageBlueprint.makeWithOverrides({
|
||||
const configApi = useApi(configApiRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isMobile && <HeaderPage title="Search" />}
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBar debounceTime={100} />
|
||||
</Grid>
|
||||
{!isMobile && (
|
||||
<Grid item xs={3}>
|
||||
<SearchType.Accordion
|
||||
name="Result Type"
|
||||
defaultValue={configApi.getOptionalString(
|
||||
'search.defaultType',
|
||||
)}
|
||||
showCounts
|
||||
types={[
|
||||
{
|
||||
value: 'software-catalog',
|
||||
name: 'Software Catalog',
|
||||
icon: <CatalogIcon />,
|
||||
},
|
||||
{
|
||||
value: 'techdocs',
|
||||
name: 'Documentation',
|
||||
icon: <DocsIcon />,
|
||||
},
|
||||
].concat(resultTypes)}
|
||||
/>
|
||||
<Paper className={classes.filters}>
|
||||
{types.includes('techdocs') && (
|
||||
<SearchFilter.Select
|
||||
className={classes.filter}
|
||||
label="Entity"
|
||||
name="name"
|
||||
values={async () => {
|
||||
// Return a list of entities which are documented.
|
||||
const { items } = await catalogApi.getEntities({
|
||||
fields: ['metadata.name'],
|
||||
filter: {
|
||||
'metadata.annotations.backstage.io/techdocs-ref':
|
||||
CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
});
|
||||
|
||||
const names = items.map(
|
||||
entity => entity.metadata.name,
|
||||
);
|
||||
names.sort();
|
||||
return names;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBar debounceTime={100} />
|
||||
</Grid>
|
||||
{!isMobile && (
|
||||
<Grid item xs={3}>
|
||||
<SearchType.Accordion
|
||||
name="Result Type"
|
||||
defaultValue={configApi.getOptionalString(
|
||||
'search.defaultType',
|
||||
)}
|
||||
showCounts
|
||||
types={[
|
||||
{
|
||||
value: 'software-catalog',
|
||||
name: 'Software Catalog',
|
||||
icon: <CatalogIcon />,
|
||||
},
|
||||
{
|
||||
value: 'techdocs',
|
||||
name: 'Documentation',
|
||||
icon: <DocsIcon />,
|
||||
},
|
||||
].concat(resultTypes)}
|
||||
/>
|
||||
<Paper className={classes.filters}>
|
||||
{types.includes('techdocs') && (
|
||||
<SearchFilter.Select
|
||||
className={classes.filter}
|
||||
label="Kind"
|
||||
name="kind"
|
||||
values={[
|
||||
'API',
|
||||
'Component',
|
||||
'Domain',
|
||||
'Group',
|
||||
'Location',
|
||||
'Resource',
|
||||
'System',
|
||||
'Template',
|
||||
'User',
|
||||
]}
|
||||
/>
|
||||
<SearchFilter.Checkbox
|
||||
className={classes.filter}
|
||||
label="Lifecycle"
|
||||
name="lifecycle"
|
||||
values={['experimental', 'production']}
|
||||
/>
|
||||
{additionalSearchFilters.map(SearchFilterComponent => (
|
||||
<SearchFilterComponent className={classes.filter} />
|
||||
))}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs>
|
||||
<SearchPagination />
|
||||
<SearchResults>
|
||||
{({ results }) => (
|
||||
<>
|
||||
{results.map((result, index) => {
|
||||
const { noTrack } = config;
|
||||
const { document, ...rest } = result;
|
||||
const SearchResultListItem =
|
||||
getResultItemComponent(result);
|
||||
return (
|
||||
<SearchResultListItem
|
||||
{...rest}
|
||||
key={index}
|
||||
result={document}
|
||||
noTrack={noTrack}
|
||||
/>
|
||||
label="Entity"
|
||||
name="name"
|
||||
values={async () => {
|
||||
// Return a list of entities which are documented.
|
||||
const { items } = await catalogApi.getEntities({
|
||||
fields: ['metadata.name'],
|
||||
filter: {
|
||||
'metadata.annotations.backstage.io/techdocs-ref':
|
||||
CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
});
|
||||
|
||||
const names = items.map(
|
||||
entity => entity.metadata.name,
|
||||
);
|
||||
})}
|
||||
</>
|
||||
names.sort();
|
||||
return names;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SearchResults>
|
||||
<SearchResultPager />
|
||||
<SearchFilter.Select
|
||||
className={classes.filter}
|
||||
label="Kind"
|
||||
name="kind"
|
||||
values={[
|
||||
'API',
|
||||
'Component',
|
||||
'Domain',
|
||||
'Group',
|
||||
'Location',
|
||||
'Resource',
|
||||
'System',
|
||||
'Template',
|
||||
'User',
|
||||
]}
|
||||
/>
|
||||
<SearchFilter.Checkbox
|
||||
className={classes.filter}
|
||||
label="Lifecycle"
|
||||
name="lifecycle"
|
||||
values={['experimental', 'production']}
|
||||
/>
|
||||
{additionalSearchFilters.map(SearchFilterComponent => (
|
||||
<SearchFilterComponent className={classes.filter} />
|
||||
))}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs>
|
||||
<SearchPagination />
|
||||
<SearchResults>
|
||||
{({ results }) => (
|
||||
<>
|
||||
{results.map((result, index) => {
|
||||
const { noTrack } = config;
|
||||
const { document, ...rest } = result;
|
||||
const SearchResultListItem =
|
||||
getResultItemComponent(result);
|
||||
return (
|
||||
<SearchResultListItem
|
||||
{...rest}
|
||||
key={index}
|
||||
result={document}
|
||||
noTrack={noTrack}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</SearchResults>
|
||||
<SearchResultPager />
|
||||
</Grid>
|
||||
</Content>
|
||||
</>
|
||||
</Grid>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
"@backstage/plugin-techdocs-common": "workspace:^",
|
||||
"@backstage/plugin-techdocs-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/ui": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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 { ApiProvider } from '@backstage/core-app-api';
|
||||
import { configApiRef, storageApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
MockStarredEntitiesApi,
|
||||
catalogApiRef,
|
||||
starredEntitiesApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import {
|
||||
TestApiRegistry,
|
||||
mockApis,
|
||||
renderInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { rootDocsRouteRef } from '../routes';
|
||||
import { NfsTechDocsIndexPage } from './NfsTechDocsIndexPage';
|
||||
|
||||
const mockCatalogApi = catalogApiMock({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'owned',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('NfsTechDocsIndexPage', () => {
|
||||
it('renders the documentation heading and subtitle', async () => {
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[
|
||||
configApiRef,
|
||||
mockApis.config({
|
||||
data: { organization: { name: 'My Company' } },
|
||||
}),
|
||||
],
|
||||
[storageApiRef, mockApis.storage()],
|
||||
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
|
||||
);
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<NfsTechDocsIndexPage />
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: 'Documentation' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/Documentation available in My Company/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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 { FC, ReactNode } from 'react';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { Page } from '@backstage/core-components';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import {
|
||||
TechDocsIndexPageProps,
|
||||
DefaultTechDocsHome,
|
||||
} from '../home/components';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const generatedSubtitle = `Documentation available in ${
|
||||
configApi.getOptionalString('organization.name') ?? 'Backstage'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
<HeaderPage title="Documentation" />
|
||||
<Box mt={2}>{generatedSubtitle}</Box>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const NfsTechDocsIndexPage = (props: TechDocsIndexPageProps) => {
|
||||
const outlet = useOutlet();
|
||||
|
||||
return (
|
||||
outlet || (
|
||||
<DefaultTechDocsHome {...props} PageWrapper={NfsTechDocsPageWrapper} />
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
* 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 { PropsWithChildren, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { HeaderLabel, Page } from '@backstage/core-components';
|
||||
import { HeaderPage } from '@backstage/ui';
|
||||
import {
|
||||
useTechDocsAddons,
|
||||
TechDocsAddonLocations as locations,
|
||||
useTechDocsReaderPage,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import {
|
||||
entityPresentationApiRef,
|
||||
EntityRefLink,
|
||||
EntityRefLinks,
|
||||
getEntityRelations,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
RELATION_OWNED_BY,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { TechDocsReaderPageContent } from '../reader/components/TechDocsReaderPageContent';
|
||||
import { TechDocsReaderPageSubheader } from '../reader/components/TechDocsReaderPageSubheader';
|
||||
import { rootRouteRef } from '../routes';
|
||||
|
||||
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
|
||||
|
||||
const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => {
|
||||
const { children } = props;
|
||||
const addons = useTechDocsAddons();
|
||||
const configApi = useApi(configApiRef);
|
||||
const entityPresentationApi = useApi(entityPresentationApiRef);
|
||||
const docsRootLink = useRouteRef(rootRouteRef)();
|
||||
const { '*': path = '' } = useParams();
|
||||
|
||||
const {
|
||||
title,
|
||||
setTitle,
|
||||
subtitle,
|
||||
setSubtitle,
|
||||
entityRef,
|
||||
metadata: { value: metadata, loading: metadataLoading },
|
||||
entityMetadata: { value: entityMetadata, loading: entityMetadataLoading },
|
||||
} = useTechDocsReaderPage();
|
||||
|
||||
useEffect(() => {
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle(metadata.site_name);
|
||||
setSubtitle(() => {
|
||||
let { site_description } = metadata;
|
||||
if (!site_description || site_description === 'None') {
|
||||
site_description = '';
|
||||
}
|
||||
return site_description;
|
||||
});
|
||||
}, [metadata, setTitle, setSubtitle]);
|
||||
|
||||
const appTitle = configApi.getOptional('app.title') || 'Backstage';
|
||||
const { locationMetadata, spec } = entityMetadata || {};
|
||||
const lifecycle = spec?.lifecycle;
|
||||
const ownedByRelations = entityMetadata
|
||||
? getEntityRelations(entityMetadata, RELATION_OWNED_BY)
|
||||
: [];
|
||||
const labels = (
|
||||
<>
|
||||
<HeaderLabel
|
||||
label={capitalize(entityMetadata?.kind || 'entity')}
|
||||
value={
|
||||
<EntityRefLink
|
||||
color="inherit"
|
||||
entityRef={entityRef}
|
||||
title={entityMetadata?.metadata.title}
|
||||
defaultKind="Component"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{ownedByRelations.length > 0 && (
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
value={
|
||||
<EntityRefLinks
|
||||
color="inherit"
|
||||
entityRefs={ownedByRelations}
|
||||
defaultKind="group"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{lifecycle ? (
|
||||
<HeaderLabel label="Lifecycle" value={String(lifecycle)} />
|
||||
) : null}
|
||||
{locationMetadata &&
|
||||
locationMetadata.type !== 'dir' &&
|
||||
locationMetadata.type !== 'file' ? (
|
||||
<HeaderLabel
|
||||
label=""
|
||||
value={
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<Grid style={{ padding: 0 }} item>
|
||||
<CodeIcon style={{ marginTop: '-25px' }} />
|
||||
</Grid>
|
||||
<Grid style={{ padding: 0 }} item>
|
||||
Source
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
url={locationMetadata.target}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const noEntMetadata = !entityMetadataLoading && entityMetadata === undefined;
|
||||
const noTdMetadata = !metadataLoading && metadata === undefined;
|
||||
if (noEntMetadata || noTdMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stringEntityRef = stringifyEntityRef(entityRef);
|
||||
const entityDisplayName =
|
||||
entityPresentationApi.forEntity(stringEntityRef).snapshot.primaryTitle;
|
||||
const removeTrailingSlash = (str: string) => str.replace(/\/$/, '');
|
||||
const normalizeAndSpace = (str: string) =>
|
||||
str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' ');
|
||||
|
||||
let techdocsTabTitleItems: string[] = [];
|
||||
if (path !== '') {
|
||||
techdocsTabTitleItems = removeTrailingSlash(path)
|
||||
.split('/')
|
||||
.map(normalizeAndSpace);
|
||||
}
|
||||
|
||||
const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle];
|
||||
const tabTitle = tabTitleItems.join(' | ');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet titleTemplate="%s">
|
||||
<title>{tabTitle}</title>
|
||||
</Helmet>
|
||||
<HeaderPage
|
||||
title={title || 'Documentation'}
|
||||
breadcrumbs={[{ label: 'Documentation', href: docsRootLink }]}
|
||||
customActions={
|
||||
<>
|
||||
{children}
|
||||
{addons.renderComponentsByLocation(locations.Header)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{(subtitle ||
|
||||
metadataLoading ||
|
||||
entityMetadataLoading ||
|
||||
entityMetadata) && (
|
||||
<Box mt={2}>
|
||||
{subtitle !== '' ? subtitle || skeleton : null}
|
||||
{entityMetadata && <Box mt={subtitle === '' ? 0 : 1}>{labels}</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type NfsTechDocsReaderLayoutProps = {
|
||||
withHeader?: boolean;
|
||||
withSearch?: boolean;
|
||||
};
|
||||
|
||||
export const NfsTechDocsReaderLayout = (
|
||||
props: NfsTechDocsReaderLayoutProps,
|
||||
) => {
|
||||
const { withSearch, withHeader = true } = props;
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
{withHeader && <NfsTechDocsReaderPageHeader />}
|
||||
<TechDocsReaderPageSubheader />
|
||||
<TechDocsReaderPageContent withSearch={withSearch} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
rootDocsRouteRef,
|
||||
rootRouteRef,
|
||||
} from '../routes';
|
||||
import { TechDocsReaderLayout } from '../reader';
|
||||
import {
|
||||
TechDocsAddons,
|
||||
techdocsApiRef,
|
||||
@@ -139,7 +140,9 @@ const techDocsPage = PageBlueprint.make({
|
||||
path: '/docs',
|
||||
routeRef: rootRouteRef,
|
||||
loader: () =>
|
||||
import('./NfsTechDocsIndexPage').then(m => <m.NfsTechDocsIndexPage />),
|
||||
import('../home/components/TechDocsIndexPage').then(m => (
|
||||
<m.TechDocsIndexPage />
|
||||
)),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -183,12 +186,9 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({
|
||||
);
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
import('../Router'),
|
||||
import('./NfsTechDocsReaderLayout'),
|
||||
]).then(([{ TechDocsReaderRouter }, { NfsTechDocsReaderLayout }]) => (
|
||||
return import('../Router').then(({ TechDocsReaderRouter }) => (
|
||||
<TechDocsReaderRouter>
|
||||
<NfsTechDocsReaderLayout
|
||||
<TechDocsReaderLayout
|
||||
withSearch={!config.withoutSearch}
|
||||
withHeader={!config.withoutHeader}
|
||||
/>
|
||||
|
||||
@@ -140,7 +140,6 @@ export type TechDocsReaderLayoutProps = {
|
||||
*/
|
||||
export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => {
|
||||
const { withSearch, withHeader = true } = props;
|
||||
|
||||
return (
|
||||
<Page themeId="documentation">
|
||||
{withHeader && <TechDocsReaderPageHeader />}
|
||||
|
||||
Reference in New Issue
Block a user