Skip to content

Commit

Permalink
Merge pull request backstage#11404 from acierto/reconfigure
Browse files Browse the repository at this point in the history
Make a possibility to configure existing components
  • Loading branch information
benjdlambert authored Jul 20, 2022
2 parents fe4116b + c38cb38 commit a9db61c
Show file tree
Hide file tree
Showing 90 changed files with 616 additions and 42 deletions.
8 changes: 8 additions & 0 deletions .changeset/famous-bikes-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@backstage/core-plugin-api': patch
---

Introduced a new experimental feature that allows you to declare plugin-wide options for your plugin by defining
`__experimentalConfigure` in your `createPlugin` options. See https://backstage.io/docs/plugins/customization.md for more information.

This is an experimental feature and it will have breaking changes in the future.
15 changes: 15 additions & 0 deletions .changeset/few-berries-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@backstage/plugin-catalog': minor
---

Plugin catalog has been modified to use an experimental feature where you can customize the title of the create button.

You can modify it by doing:

```typescript jsx
import { catalogPlugin } from '@backstage/plugin-catalog';

catalogPlugin.__experimentalReconfigure({
createButtonTitle: 'New',
});
```
68 changes: 68 additions & 0 deletions docs/plugins/customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
id: customization
title: Customization
description: Documentation on adding a customization logic to the plugin
---

## Overview

The Backstage core logic provides a possibility to make the component customizable in such a way that the application
developer can redefine the labels, icons, elements or even completely replace the component. It's up to each plugin
to decide what can be customized.

## For a plugin developer

When you are creating your plugin, you have a possibility to use a metadata field and define there all
customizable elements. For example

```typescript jsx
const plugin = createPlugin({
id: 'my-plugin',
__experimentalConfigure(
options?: CatalogInputPluginOptions,
): CatalogPluginOptions {
const defaultOptions = {
createButtonTitle: 'Create',
};
return { ...defaultOptions, ...options };
},
});
```

And the rendering part of the exposed component can retrieve that metadata as:

```typescript jsx
export type CatalogPluginOptions = {
createButtonTitle: string;
};

export type CatalogInputPluginOptions = {
createButtonTitle: string;
};

export const useCatalogPluginOptions = () =>
usePluginOptions<CatalogPluginOptions>();

export function DefaultMyPluginWelcomePage() {
const { createButtonTitle } = useCatalogPluginOptions();

return (
<div>
<button>{createButtonTitle}</button>
</div>
);
}
```

## For an application developer using the plugin

The way to reconfigure the default values provided by the plugin you can do it via reconfigure method, defined on the
plugin. Example:

```typescript jsx
import { myPlugin } from '@backstage/my-plugin';

myPlugin.__experimentalReconfigure({
createButtonTitle: 'New',
});
```
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"@backstage/plugin-azure-devops": "^0.1.23",
"@backstage/plugin-apache-airflow": "^0.2.0",
"@backstage/plugin-badges": "^0.2.31",
"@backstage/plugin-catalog": "^1.4.0",
"@backstage/plugin-catalog-common": "^1.0.4",
"@backstage/plugin-catalog-graph": "^0.2.19",
"@backstage/plugin-catalog-import": "^0.8.10",
Expand Down Expand Up @@ -70,6 +69,7 @@
"@roadiehq/backstage-plugin-github-insights": "^2.0.0",
"@roadiehq/backstage-plugin-github-pull-requests": "^2.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^2.0.0",
"@internal/plugin-catalog-customized": "0.0.0",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ import {
} from '@backstage/core-components';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import { AzurePullRequestsPage } from '@backstage/plugin-azure-devops';

import {
CatalogEntityPage,
CatalogIndexPage,
catalogPlugin,
} from '@backstage/plugin-catalog';
} from '@internal/plugin-catalog-customized';

import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import {
CatalogImportPage,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/catalog/EntityPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { EntityLayout } from '@backstage/plugin-catalog';
import { EntityLayout } from '@internal/plugin-catalog-customized';
import {
EntityProvider,
starredEntitiesApiRef,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/catalog/EntityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import {
isComponentType,
isKind,
isOrphan,
} from '@backstage/plugin-catalog';
} from '@internal/plugin-catalog-customized';
import {
Direction,
EntityCatalogGraphCard,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/search/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
useContent,
} from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
import {
catalogApiRef,
CATALOG_FILTER_EXISTS,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/search/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
useSidebarPinState,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
import {
catalogApiRef,
CATALOG_FILTER_EXISTS,
Expand Down
28 changes: 26 additions & 2 deletions packages/core-plugin-api/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,15 @@ export type BackstageIdentityResponse = {
export type BackstagePlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {},
PluginInputOptions extends {} = {},
> = {
getId(): string;
getApis(): Iterable<AnyApiFactory>;
getFeatureFlags(): Iterable<PluginFeatureFlagConfig>;
provide<T>(extension: Extension<T>): T;
routes: Routes;
externalRoutes: ExternalRoutes;
__experimentalReconfigure(options: PluginInputOptions): void;
};

// @public
Expand Down Expand Up @@ -303,9 +305,10 @@ export function createExternalRouteRef<
export function createPlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {},
PluginInputOptions extends {} = {},
>(
config: PluginConfig<Routes, ExternalRoutes>,
): BackstagePlugin<Routes, ExternalRoutes>;
config: PluginConfig<Routes, ExternalRoutes, PluginInputOptions>,
): BackstagePlugin<Routes, ExternalRoutes, PluginInputOptions>;

// @public
export function createReactExtension<
Expand Down Expand Up @@ -621,19 +624,35 @@ export type PendingOAuthRequest = {
export type PluginConfig<
Routes extends AnyRoutes,
ExternalRoutes extends AnyExternalRoutes,
PluginInputOptions extends {},
> = {
id: string;
apis?: Iterable<AnyApiFactory>;
routes?: Routes;
externalRoutes?: ExternalRoutes;
featureFlags?: PluginFeatureFlagConfig[];
__experimentalConfigure?(options?: PluginInputOptions): {};
};

// @public
export type PluginFeatureFlagConfig = {
name: string;
};

// @alpha
export interface PluginOptionsProviderProps {
// (undocumented)
children: ReactNode;
// (undocumented)
plugin?: BackstagePlugin;
}

// @alpha
export const PluginProvider: ({
children,
plugin,
}: PluginOptionsProviderProps) => JSX.Element;

// @public
export type ProfileInfo = {
email?: string;
Expand Down Expand Up @@ -734,6 +753,11 @@ export function useElementFilter<T>(
dependencies?: any[],
): T;

// @alpha
export function usePluginOptions<
TPluginOptions extends {} = {},
>(): TPluginOptions;

// @public
export function useRouteRef<Optional extends boolean, Params extends AnyParams>(
routeRef: ExternalRouteRef<Params, Optional>,
Expand Down
9 changes: 6 additions & 3 deletions packages/core-plugin-api/src/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
*/

import React, { lazy, Suspense } from 'react';
import { AnalyticsContext } from '../analytics/AnalyticsContext';
import { AnalyticsContext } from '../analytics';
import { useApp } from '../app';
import { RouteRef, useRouteRef } from '../routing';
import { attachComponentData } from './componentData';
import { Extension, BackstagePlugin } from '../plugin/types';
import { Extension, BackstagePlugin } from '../plugin';
import { PluginErrorBoundary } from './PluginErrorBoundary';
import { PluginProvider } from '../plugin-options';

/**
* Lazy or synchronous retrieving of extension components.
Expand Down Expand Up @@ -245,7 +246,9 @@ export function createReactExtension<
...(mountPoint && { routeRef: mountPoint.id }),
}}
>
<Component {...props} />
<PluginProvider plugin={plugin}>
<Component {...props} />
</PluginProvider>
</AnalyticsContext>
</PluginErrorBoundary>
</Suspense>
Expand Down
1 change: 1 addition & 0 deletions packages/core-plugin-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

export * from './analytics';
export * from './apis';
export * from './plugin-options';
export * from './app';
export * from './extensions';
export * from './icons';
Expand Down
18 changes: 18 additions & 0 deletions packages/core-plugin-api/src/plugin-options/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { usePluginOptions, PluginProvider } from './usePluginOptions';
export type { PluginOptionsProviderProps } from './usePluginOptions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2021 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 React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { usePluginOptions, PluginProvider } from './usePluginOptions';
import { createPlugin } from '../plugin';

describe('usePluginOptions', () => {
it('should provide a versioned value to hook', () => {
type TestInputPluginOptions = {
'key-1': string;
};

type TestPluginOptions = {
'key-1': string;
'key-2': string;
};

const plugin = createPlugin({
id: 'my-plugin',
__experimentalConfigure(_: TestInputPluginOptions): TestPluginOptions {
return { 'key-1': 'value-1', 'key-2': 'value-2' };
},
});

const rendered = renderHook(() => usePluginOptions(), {
wrapper: ({ children }) => (
<PluginProvider plugin={plugin}>{children}</PluginProvider>
),
});

const config = rendered.result.current;

expect(config).toEqual({
'key-1': 'value-1',
'key-2': 'value-2',
});
});
});
Loading

0 comments on commit a9db61c

Please sign in to comment.