chore: extension overrides spike

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-06-25 14:25:22 +02:00
parent 1f5094f1b7
commit f749bb8fa9
4 changed files with 194 additions and 36 deletions
@@ -26,6 +26,7 @@ import {
coreExtensionData,
createExtension,
createExtensionInput,
createExtensionKind,
createExtensionOverrides,
} from '@backstage/frontend-plugin-api';
import { getComponentData } from '@backstage/core-plugin-api';
@@ -128,21 +129,80 @@ export function convertLegacyApp(
};
},
});
const CoreNavOverride = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
output: {},
const CoreNavOverride = CurrentCoreNav.override({
// namespace: 'app',
// name: 'nav',
// attachTo: { id: 'app/layout', input: 'nav' },
// output: {},
factory: () => ({}),
disabled: true,
});
createExtensionOverride({
extension: CurrentCoreNav,
factory: () => null,
});
const collectedRoutes = collectLegacyRoutes(routesEl);
return [
...collectedRoutes,
createExtensionOverrides({
extensions: [CoreLayoutOverride, CoreNavOverride],
extensions: [CoreNavOverride, CoreNavOverride],
}),
];
}
const EntityCardExtension = createExtensionKind({
kind: 'entity-card',
attachTo: { id: 'entity-card', input: 'default' },
inputs: {
loader: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }, props: { title: string }) {
console.log(inputs.loader);
return {
element: React.createElement('h1'),
};
},
});
const GithubCard = EntityCardExtension.new({
props: {
title: 'GitHub Card',
},
factory({ inputs: { loader } }) {
console.log(loader);
return {
element: React.createElement('h2'),
};
},
});
GithubCard.override({
attachTo: { id: 'entity-card', input: 'github' },
inputs: {
loader: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: false },
),
loader2: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
factory({ originalFactory, inputs }) {
inputs.loader2;
inputs.loader;
return originalFactory({
inputsOverride: inputs,
});
},
});
@@ -291,4 +291,37 @@ describe('createExtension', () => {
'ExtensionDefinition{namespace=test,attachTo=root@default}',
);
});
describe('override', () => {
it('should create an extension override', () => {
const extension = createExtension({
namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: {
foo: stringData,
},
factory() {
return {
foo: 'bar',
};
},
});
const override = extension.override({
attachTo: {
id: 'root',
input: 'default2',
},
factory() {
return {
foo: 'baz',
};
},
});
expect(String(override)).toBe(
'ExtensionDefinition{namespace=test,attachTo=root@default2}',
);
});
});
});
@@ -99,7 +99,33 @@ export interface CreateExtensionOptions<
}
/** @public */
export interface ExtensionDefinition<TConfig> {
export interface ExtensionDefinitionOverrides<
TOriginalConfig,
TOriginalInputs extends AnyExtensionInputMap,
TOverrideInputs extends AnyExtensionInputMap,
> {
readonly disabled?: boolean;
readonly attachTo?: { id: string; input: string };
// TODO: inputs (only adding to the list? or redefine arity and superset of old type on existing input?)
// TODO: config (any merging needed on there?)
inputs?: TOverrideInputs;
factory?(options: {
node: AppNode;
config: TOriginalConfig;
inputs: Expand<ResolvedExtensionInputs<TOriginalInputs & TOverrideInputs>>;
originalFactory: (originalFactoryOptions?: {
inputsOverride?: ResolvedExtensionInputs<
TOriginalInputs & TOverrideInputs
>;
}) => ExtensionDataValues<any>;
}): ExtensionDataValues<any>;
}
/** @public */
export interface ExtensionDefinition<
TConfig,
TInputs extends AnyExtensionInputMap,
> {
$$type: '@backstage/ExtensionDefinition';
readonly kind?: string;
readonly namespace?: string;
@@ -107,13 +133,18 @@ export interface ExtensionDefinition<TConfig> {
readonly attachTo: { id: string; input: string };
readonly disabled: boolean;
readonly configSchema?: PortableSchema<TConfig>;
override<TOverrideInputs extends AnyExtensionInputMap>(
overrides: ExtensionDefinitionOverrides<TConfig, TInputs, TOverrideInputs>,
): ExtensionDefinition<TConfig, TInputs>;
}
/** @internal */
export interface InternalExtensionDefinition<TConfig>
extends ExtensionDefinition<TConfig> {
export interface InternalExtensionDefinition<
TConfig,
TInputs extends AnyExtensionInputMap,
> extends ExtensionDefinition<TConfig, TInputs> {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly inputs: TInputs;
readonly output: AnyExtensionDataMap;
factory(context: {
node: AppNode;
@@ -123,10 +154,13 @@ export interface InternalExtensionDefinition<TConfig>
}
/** @internal */
export function toInternalExtensionDefinition<TConfig>(
overrides: ExtensionDefinition<TConfig>,
): InternalExtensionDefinition<TConfig> {
const internal = overrides as InternalExtensionDefinition<TConfig>;
export function toInternalExtensionDefinition<
TConfig,
TInputs extends AnyExtensionInputMap,
>(
overrides: ExtensionDefinition<TConfig, TInputs>,
): InternalExtensionDefinition<TConfig, TInputs> {
const internal = overrides as InternalExtensionDefinition<TConfig, TInputs>;
if (internal.$$type !== '@backstage/ExtensionDefinition') {
throw new Error(
`Invalid extension definition instance, bad type '${internal.$$type}'`,
@@ -147,38 +181,69 @@ export function createExtension<
TConfig = never,
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
): ExtensionDefinition<TConfig> {
): ExtensionDefinition<TConfig, TInputs> {
const {
kind,
namespace,
name,
attachTo,
disabled = false,
inputs = {} as TInputs,
output,
configSchema,
factory,
} = options;
return {
$$type: '@backstage/ExtensionDefinition',
version: 'v1',
kind: options.kind,
namespace: options.namespace,
name: options.name,
attachTo: options.attachTo,
disabled: options.disabled ?? false,
inputs: options.inputs ?? {},
output: options.output,
configSchema: options.configSchema,
factory({ inputs, ...rest }) {
kind,
namespace,
name,
attachTo,
disabled,
inputs,
output,
configSchema,
factory({ inputs: factoryInputs, ...rest }) {
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
return options.factory({
inputs: inputs as Expand<ResolvedExtensionInputs<TInputs>>,
return factory({
inputs: factoryInputs as Expand<ResolvedExtensionInputs<TInputs>>,
...rest,
});
},
toString() {
const parts: string[] = [];
if (options.kind) {
parts.push(`kind=${options.kind}`);
if (kind) {
parts.push(`kind=${kind}`);
}
if (options.namespace) {
parts.push(`namespace=${options.namespace}`);
if (namespace) {
parts.push(`namespace=${namespace}`);
}
if (options.name) {
parts.push(`name=${options.name}`);
if (name) {
parts.push(`name=${name}`);
}
parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`);
parts.push(`attachTo=${attachTo.id}@${attachTo.input}`);
return `ExtensionDefinition{${parts.join(',')}}`;
},
} as InternalExtensionDefinition<TConfig>;
override<TOverrideInputs extends AnyExtensionInputMap>(
overrides: ExtensionDefinitionOverrides<
TConfig,
TInputs,
TOverrideInputs
>,
): ExtensionDefinition<TConfig, TInputs> {
return createExtension({
kind,
namespace,
name,
attachTo: overrides.attachTo ?? attachTo,
disabled: overrides.disabled ?? disabled,
inputs: { ...inputs, ...overrides.inputs },
output,
configSchema,
factory,
});
},
} as InternalExtensionDefinition<TConfig, TInputs>;
}
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AppNode } from '../apis';
import { PortableSchema } from '../schema';
import { Expand } from '../types';
@@ -21,6 +20,7 @@ import {
AnyExtensionDataMap,
AnyExtensionInputMap,
ExtensionDataValues,
ExtensionDefinition,
ResolvedExtensionInputs,
createExtension,
} from './createExtension';
@@ -107,7 +107,7 @@ export class ExtensionKind<
},
options: TOptions,
): Expand<ExtensionDataValues<TOutput>>;
}) {
}): ExtensionDefinition<TConfig, TInputs> {
return createExtension({
kind: this.options.kind,
namespace: args.namespace ?? this.options.namespace,