Migrate techdocs-cli-embedded-app to NFS

Signed-off-by: Gabriel Dugny <gabriel.dugny@believe.com>
This commit is contained in:
Gabriel Dugny
2026-01-11 17:05:51 +01:00
parent 52d6767865
commit 27798df0a3
18 changed files with 204 additions and 159 deletions
+16
View File
@@ -0,0 +1,16 @@
---
'@backstage/plugin-techdocs': minor
---
Add 2 config elements to extension "page:techdocs/reader" to configure default layout `withSearch` and `withHeader`. Default are unchanged to `true`.
E.g. to disable the search and header on the Techdocs Reader Page:
```yaml
app:
extensions:
- page:techdocs/reader:
config:
withSearch: false
withHeader: false
```
+5
View File
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Migrate the Techdocs CLI embedded app to the New Frontend System (NFS)
@@ -1,6 +1,11 @@
app:
title: Techdocs Preview App
baseUrl: http://localhost:3000
extensions:
- sign-in-page:app: false
- page:techdocs/reader:
config:
withSearch: false
backend:
baseUrl: http://localhost:3000
@@ -1,17 +1,9 @@
# Knip report
## Unused dependencies (2)
## Unused devDependencies (2)
| Name | Location | Severity |
| :-------- | :----------- | :------- |
| react-use | packages/techdocs-cli-embedded-app/package.json | error |
| history | packages/techdocs-cli-embedded-app/package.json | error |
## Unused devDependencies (3)
| Name | Location | Severity |
| :-------------------------- | :----------- | :------- |
| @testing-library/user-event | packages/techdocs-cli-embedded-app/package.json | error |
| @testing-library/dom | packages/techdocs-cli-embedded-app/package.json | error |
| cross-env | packages/techdocs-cli-embedded-app/package.json | error |
| Name | Location | Severity |
| :-------------------------- | :---------------- | :------- |
| @testing-library/user-event | package.json:61:6 | error |
| cross-env | package.json:64:6 | error |
@@ -32,13 +32,13 @@
},
"prettier": "@backstage/cli/config/prettier",
"dependencies": {
"@backstage/app-defaults": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-defaults": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-techdocs": "workspace:^",
@@ -48,11 +48,9 @@
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4"
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
@@ -15,13 +15,27 @@
*/
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
import app from './App';
jest.mock('./config', () => ({
configLoader: async () => [
{
data: {
app: { title: 'Test' },
app: {
title: 'Test',
extensions: [
{
'sign-in-page:app': false,
},
{
'page:techdocs/reader': {
config: {
withSearch: false,
},
},
},
],
},
backend: { baseUrl: 'http://localhost:7007' },
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
@@ -34,7 +48,7 @@ jest.mock('./config', () => ({
describe('App', () => {
it('should render', async () => {
const rendered = await renderWithEffects(<App />);
const rendered = await renderWithEffects(app);
expect(rendered.getByText('Docs Preview')).toBeInTheDocument();
});
});
+30 -74
View File
@@ -14,85 +14,41 @@
* limitations under the License.
*/
import { Navigate, Route } from 'react-router-dom';
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
techdocsPlugin,
} from '@backstage/plugin-techdocs';
import {
createTechDocsAddonExtension,
TechDocsAddons,
TechDocsAddonLocations,
} from '@backstage/plugin-techdocs-react';
import { createApp } from '@backstage/app-defaults';
import { FlatRoutes } from '@backstage/core-app-api';
import { CatalogEntityPage } from '@backstage/plugin-catalog';
import { createApp } from '@backstage/frontend-defaults';
import { ConfigReader } from '@backstage/core-app-api';
import catalogPlugin from '@backstage/plugin-catalog/alpha';
import { apis } from './apis';
import * as plugins from './plugins';
import { configLoader } from './config';
import { Root } from './components/Root';
import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage';
import { TechDocsLiveReload } from './LiveReloadAddon';
const app = createApp({
apis,
configLoader,
plugins: Object.values(plugins),
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SidebarContent } from './components/Root/Root';
import {
techDocsThemeToggleAddonModule,
techdocsLiveReloadAddonModule,
} from './addons';
const appPlugin = createFrontendModule({
pluginId: 'app',
extensions: [...apis, SidebarContent],
});
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
const app = createApp({
features: [
appPlugin,
techdocsPlugin,
catalogPlugin,
techDocsThemeToggleAddonModule,
techdocsLiveReloadAddonModule,
],
advanced: {
async configLoader() {
const appConfigs = await configLoader();
return { config: ConfigReader.fromConfigs(appConfigs) };
},
},
});
const ThemeToggleAddon = techdocsPlugin.provide(
createTechDocsAddonExtension({
name: 'ThemeToggleAddon',
component: TechDocsThemeToggle,
location: TechDocsAddonLocations.Header,
}),
);
const LiveReloadAddon = techdocsPlugin.provide(
createTechDocsAddonExtension({
name: 'LiveReloadAddon',
component: TechDocsLiveReload,
location: TechDocsAddonLocations.Content,
}),
);
const routes = (
<FlatRoutes>
<Navigate key="/" to="/docs/default/component/local/" />
{/* we need this route as TechDocs header links relies on it */}
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
/>
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
>
{techDocsPage}
<TechDocsAddons>
<LiveReloadAddon />
<ThemeToggleAddon />
</TechDocsAddons>
</Route>
</FlatRoutes>
);
const App = () => (
<AppProvider>
<AppRouter>
<Root>{routes}</Root>
</AppRouter>
</AppProvider>
);
export default App;
export default app.createRoot();
@@ -0,0 +1,48 @@
/*
* 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 { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha';
import { TechDocsAddonLocations } from '@backstage/plugin-techdocs-react';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { TechDocsThemeToggle } from './components/TechDocsPage';
import { TechDocsLiveReload } from './components/LiveReload/LiveReloadAddon';
const techDocsThemeToggleAddonExtension = AddonBlueprint.make({
name: 'techdocs-theme-toggle-addon',
params: {
name: 'ThemeToggleAddon',
component: TechDocsThemeToggle,
location: TechDocsAddonLocations.Header,
},
});
export const techDocsThemeToggleAddonModule = createFrontendModule({
pluginId: 'techdocs',
extensions: [techDocsThemeToggleAddonExtension],
});
const techdocsLiveReloadAddonExtension = AddonBlueprint.make({
name: 'techdocs-live-reload-addon',
params: {
name: 'LiveReloadAddon',
component: TechDocsLiveReload,
location: TechDocsAddonLocations.Content,
},
});
export const techdocsLiveReloadAddonModule = createFrontendModule({
pluginId: 'techdocs',
extensions: [techdocsLiveReloadAddonExtension],
});
+49 -38
View File
@@ -13,6 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ApiBlueprint,
configApiRef,
DiscoveryApi,
discoveryApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/frontend-plugin-api';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
@@ -20,15 +28,6 @@ import {
scmIntegrationsApiRef,
ScmIntegrationsApi,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
DiscoveryApi,
discoveryApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import {
SyncResult,
TechDocsApi,
@@ -158,38 +157,50 @@ class TechDocsDevApi implements TechDocsApi {
}
}
export const apis: AnyApiFactory[] = [
createApiFactory({
api: techdocsStorageApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevStorageApi({
configApi,
discoveryApi,
identityApi,
export const apis = [
ApiBlueprint.make({
name: 'techdocs-dev-storage',
params: defineParams =>
defineParams({
api: techdocsStorageApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevStorageApi({
configApi,
discoveryApi,
identityApi,
}),
}),
}),
createApiFactory({
api: techdocsApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevApi({
configApi,
discoveryApi,
identityApi,
ApiBlueprint.make({
name: 'techdocs-dev',
params: defineParams =>
defineParams({
api: techdocsApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, discoveryApi, identityApi }) =>
new TechDocsDevApi({
configApi,
discoveryApi,
identityApi,
}),
}),
}),
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
ApiBlueprint.make({
name: 'scm-integrations',
params: defineParams =>
defineParams({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
}),
];
@@ -30,6 +30,7 @@ import {
useSidebarOpenState,
Link,
} from '@backstage/core-components';
import { NavContentBlueprint } from '@backstage/frontend-plugin-api';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -79,3 +80,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
{children}
</SidebarPage>
);
export const SidebarContent = NavContentBlueprint.make({
params: {
component: ({}) => <Root />,
},
});
@@ -25,7 +25,7 @@ import IconButton from '@material-ui/core/IconButton';
import LightIcon from '@material-ui/icons/Brightness7';
import DarkIcon from '@material-ui/icons/Brightness4';
import { appThemeApiRef, useApi } from '@backstage/core-plugin-api';
import { appThemeApiRef, useApi } from '@backstage/frontend-plugin-api';
import {
TechDocsReaderPage,
@@ -93,7 +93,7 @@ export const TechDocsThemeToggle = () => {
);
};
const DefaultTechDocsPage = () => {
export const DefaultTechDocsPage = () => {
return (
<TechDocsReaderPage>
<TechDocsReaderPageHeader />
@@ -19,4 +19,4 @@ import ReactDOM from 'react-dom/client';
import '@backstage/ui/css/styles.css';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
ReactDOM.createRoot(document.getElementById('root')!).render(App);
@@ -1,17 +0,0 @@
/*
* Copyright 2020 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.
*/
export { plugin as TechDocsPlugin } from '@backstage/plugin-techdocs';
+4
View File
@@ -265,9 +265,13 @@ const _default: OverridableFrontendPlugin<
}>;
'page:techdocs/reader': OverridableExtensionDefinition<{
config: {
withSearch: boolean;
withHeader: boolean;
path: string | undefined;
};
configInput: {
withSearch?: boolean | undefined;
withHeader?: boolean | undefined;
path?: string | undefined;
};
output:
+11 -2
View File
@@ -152,7 +152,13 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({
inputs: {
addons: createExtensionInput([AddonBlueprint.dataRefs.addon]),
},
factory(originalFactory, { inputs }) {
config: {
schema: {
withSearch: z => z.boolean().default(true),
withHeader: z => z.boolean().default(true),
},
},
factory(originalFactory, { inputs, config }) {
const addons = inputs.addons.map(output => {
const options = output.get(AddonBlueprint.dataRefs.addon);
const Addon = options.component;
@@ -166,7 +172,10 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({
loader: async () =>
await import('../Router').then(({ TechDocsReaderRouter }) => (
<TechDocsReaderRouter>
<TechDocsReaderLayout />
<TechDocsReaderLayout
withSearch={config.withSearch}
withHeader={config.withHeader}
/>
<TechDocsAddons>{addons}</TechDocsAddons>
</TechDocsReaderRouter>
)),
+2 -4
View File
@@ -47899,13 +47899,13 @@ __metadata:
version: 0.0.0-use.local
resolution: "techdocs-cli-embedded-app@workspace:packages/techdocs-cli-embedded-app"
dependencies:
"@backstage/app-defaults": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-defaults": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-catalog": "workspace:^"
"@backstage/plugin-techdocs": "workspace:^"
@@ -47922,11 +47922,9 @@ __metadata:
"@types/react": "npm:*"
"@types/react-dom": "npm:*"
cross-env: "npm:^10.0.0"
history: "npm:^5.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.3.0"
react-use: "npm:^17.2.4"
languageName: unknown
linkType: soft