Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions frontend/__tests__/module/k8s/k8s-models.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { referenceFor, referenceForCRD, referenceForOwnerRef, referenceForModel, kindForReference, versionForReference } from '../../../public/module/k8s';
import { referenceFor, referenceForCRD, referenceForOwnerRef, referenceForModel, kindForReference, versionForReference, modelsToMap } from '../../../public/module/k8s';
import { testNamespace, testClusterServiceVersion, testCRD, testOwnedResourceInstance } from '../../../__mocks__/k8sResourcesMocks';
import { PodModel, DeploymentModel } from '../../../public/models';
import { PodModel, DeploymentModel, SubscriptionModel, PrometheusModel } from '../../../public/models';

describe('referenceFor', () => {

Expand Down Expand Up @@ -56,3 +56,20 @@ describe('versionForReference', () => {
expect(versionForReference(referenceFor(testClusterServiceVersion))).toEqual('v1alpha1');
});
});

describe('modelsToMap', () => {

it('returns a map with keys based on model.kind for models with crd:false', () => {
expect(modelsToMap([ PodModel, DeploymentModel ]).toObject()).toEqual({
[PodModel.kind]: PodModel,
[DeploymentModel.kind]: DeploymentModel,
});
});

it('returns a map with keys based on referenceForModel for models with crd:true', () => {
expect(modelsToMap([ SubscriptionModel, PrometheusModel ]).toObject()).toEqual({
[referenceForModel(SubscriptionModel)]: SubscriptionModel,
[referenceForModel(PrometheusModel)]: PrometheusModel,
});
});
});
15 changes: 15 additions & 0 deletions frontend/packages/console-demo-plugin/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { K8sKind } from '@console/internal/module/k8s';

export const FooBarModel: K8sKind = {
apiGroup: 'test.io',
apiVersion: 'v1alpha1',
kind: 'FooBar',
label: 'Foo Bar',
labelPlural: 'Foo Bars',
path: 'foobars',
plural: 'foobars',
abbr: 'FOOBAR',
namespaced: true,
id: 'foobar',
crd: true,
};
12 changes: 12 additions & 0 deletions frontend/packages/console-demo-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as _ from 'lodash-es';

import {
Plugin,
ModelDefinition,
ModelFeatureFlag,
HrefNavItem,
ResourceNSNavItem,
Expand All @@ -12,7 +15,10 @@ import {
import { PodModel } from '@console/internal/models';
import { FLAGS } from '@console/internal/const';

import * as models from './models';

type ConsumedExtensions =
| ModelDefinition
| ModelFeatureFlag
| HrefNavItem
| ResourceNSNavItem
Expand All @@ -21,6 +27,12 @@ type ConsumedExtensions =
| ResourceDetailPage;

const plugin: Plugin<ConsumedExtensions> = [
{
type: 'ModelDefinition',
properties: {
models: _.values(models),
},
},
{
type: 'FeatureFlag/Model',
properties: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import {
Package,
PluginPackage,
isValidPluginPackage,
resolveActivePlugins,
isPluginPackage,
getActivePluginPackages,
getActivePluginsModule,
} from '..';

const templatePackage: Package = { name: 'test', version: '1.2.3', readme: '', _id: '@' };

describe('codegen', () => {

describe('isValidPluginPackage', () => {
describe('isPluginPackage', () => {
it('returns false if package.consolePlugin is missing', () => {
expect(isValidPluginPackage({
expect(isPluginPackage({
...templatePackage,
})).toBe(false);
});

it('returns false if package.consolePlugin.entry is missing', () => {
expect(isValidPluginPackage({
expect(isPluginPackage({
...templatePackage,
consolePlugin: {},
})).toBe(false);
});

it('returns false if package.consolePlugin.entry is an empty string', () => {
expect(isValidPluginPackage({
expect(isPluginPackage({
...templatePackage,
consolePlugin: { entry: '' },
})).toBe(false);
});

it('returns true if package.consolePlugin.entry is not an empty string', () => {
expect(isValidPluginPackage({
it('returns true if package.consolePlugin.entry is a non-empty string', () => {
expect(isPluginPackage({
...templatePackage,
consolePlugin: { entry: 'plugin.ts' },
})).toBe(true);
});
});

describe('resolveActivePlugins', () => {
describe('getActivePluginPackages', () => {
it('filters out packages which are not listed in appPackage.dependencies', () => {
const appPackage: Package = {
...templatePackage,
Expand All @@ -65,14 +65,14 @@ describe('codegen', () => {
},
];

expect(resolveActivePlugins(appPackage, pluginPackages)).toEqual([
expect(getActivePluginPackages(appPackage, pluginPackages)).toEqual([
{ ...pluginPackages[0] },
]);
});
});

describe('getActivePluginsModule', () => {
it('returns the source of a module that exports the list of active plugins', () => {
it('returns module source that exports the list of active plugins', () => {
const pluginPackages: PluginPackage[] = [
{
...templatePackage,
Expand All @@ -88,16 +88,17 @@ describe('codegen', () => {
},
];

const expectedModule = `
expect(getActivePluginsModule(pluginPackages)).toBe(`
const activePlugins = [];

import plugin_0 from 'bar/src/plugin.ts';
activePlugins.push(plugin_0);
activePlugins.push({ name: 'bar', extensions: plugin_0 });

import plugin_1 from 'qux-plugin/index.ts';
activePlugins.push(plugin_1);
export default activePlugins;
`.replace(/^\s+/gm, '');
activePlugins.push({ name: 'qux-plugin', extensions: plugin_1 });

expect(getActivePluginsModule(pluginPackages)).toBe(expectedModule);
export default activePlugins;
`.replace(/^\s+/gm, ''));
});
});

Expand Down
40 changes: 21 additions & 19 deletions frontend/packages/console-plugin-sdk/src/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,59 @@ import * as readPkg from 'read-pkg';

export type Package = readPkg.NormalizedPackageJson;

export interface PluginPackage extends Package {
export type PluginPackage = Package & {
consolePlugin: {
entry: string;
}
}
};

export function isValidPluginPackage(pkg: Package): pkg is PluginPackage {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed earlier, I've adapted function declarations to const since the latter form seems to be preferred.

/**
* Return `true` if the given package represents a Console plugin.
*/
export const isPluginPackage = (pkg: Package): pkg is PluginPackage => {
if (!(pkg as PluginPackage).consolePlugin) {
return false;
}

const entry = (pkg as PluginPackage).consolePlugin.entry;
return typeof entry === 'string' && entry.length > 0;
}
return typeof entry === 'string' && !!entry;
};

/**
* Read package metadata and detect any plugins.
*
* @param packageFiles Paths to `package.json` files (all the monorepo packages).
*/
export function readPackages(packageFiles: string[]) {
export const readPackages = (packageFiles: string[]) => {
const pkgList: Package[] = packageFiles.map(file => readPkg.sync({ cwd: path.dirname(file), normalize: true }));

return {
appPackage: pkgList.find(pkg => pkg.name === '@console/app'),
pluginPackages: pkgList.filter(isValidPluginPackage),
pluginPackages: pkgList.filter(isPluginPackage),
};
}
};

/**
* Resolve the list of active plugins.
* Get the list of plugins to be used for the build.
*/
export function resolveActivePlugins(appPackage: Package, pluginPackages: PluginPackage[]) {
export const getActivePluginPackages = (appPackage: Package, pluginPackages: PluginPackage[]) => {
return pluginPackages.filter(pkg => appPackage.dependencies[pkg.name] === pkg.version);
}
};

/**
* Generate the "active plugins" module source.
* Generate the `@console/active-plugins` module source.
*/
export function getActivePluginsModule(activePluginPackages: PluginPackage[]): string {
export const getActivePluginsModule = (pluginPackages: PluginPackage[]) => {
let output = `
const activePlugins = [];
`;

for (const pkg of activePluginPackages) {
const importName = `plugin_${activePluginPackages.indexOf(pkg)}`;
const importPath = `${pkg.name}/${pkg.consolePlugin.entry}`;
for (const pkg of pluginPackages) {
const importName = `plugin_${pluginPackages.indexOf(pkg)}`;
output = `
${output}
import ${importName} from '${importPath}';
activePlugins.push(${importName});
import ${importName} from '${pkg.name}/${pkg.consolePlugin.entry}';
activePlugins.push({ name: '${pkg.name}', extensions: ${importName} });
`;
}

Expand All @@ -65,4 +67,4 @@ export function getActivePluginsModule(activePluginPackages: PluginPackage[]): s
`;

return output.replace(/^\s+/gm, '');
}
};
26 changes: 19 additions & 7 deletions frontend/packages/console-plugin-sdk/src/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import * as _ from 'lodash-es';
import { Extension, PluginList, isNavItem, isResourcePage, isFeatureFlag } from './typings';

import {
Extension,
ActivePlugin,
isModelDefinition,
isFeatureFlag,
isNavItem,
isResourcePage,
} from './typings';

/**
* Registry used to query for Console extensions.
Expand All @@ -8,8 +16,16 @@ export class ExtensionRegistry {

private readonly extensions: Extension<any>[];

public constructor(plugins: PluginList) {
this.extensions = _.flatMap(plugins);
public constructor(plugins: ActivePlugin[]) {
this.extensions = _.flatMap(plugins.map(p => p.extensions));
}

public getModelDefinitions() {
return this.extensions.filter(isModelDefinition);
}

public getFeatureFlags() {
return this.extensions.filter(isFeatureFlag);
}

public getNavItems(section: string) {
Expand All @@ -20,8 +36,4 @@ export class ExtensionRegistry {
return this.extensions.filter(isResourcePage);
}

public getFeatureFlags() {
return this.extensions.filter(isFeatureFlag);
}

}
27 changes: 18 additions & 9 deletions frontend/packages/console-plugin-sdk/src/typings/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
/**
* An extension of the Console web application.
* An extension of the Console application.
*
* Each extension is a realization (instance) of an extension `type` using the
* parameters provided via the `properties` object.
*
* Core extension types should follow `Category` or `Category/Specialization`
* format, e.g. `NavItem/Href`.
* The value of extension `type` should be formatted in a way that describes
* the broader category as well as any specialization(s), for example:
*
* @todo(vojtech) write ESLint rule to guard against extension type duplicity
* - `ModelDefinition`
* - `NavItem/Href`
* - `Dashboards/Overview/Utilization`
*
* TODO(vojtech): write ESLint rule to guard against extension type duplicity
*/
export interface Extension<P> {
export type Extension<P> = {
type: string;
properties: P;
}
};

/**
* A plugin is simply a list of extensions.
* From plugin author perspective, a plugin is simply a list of extensions.
*
* Plugin metadata is stored in the `package.json` file of the corresponding
* monorepo package. The `consolePlugin.entry` path should point to a module
Expand Down Expand Up @@ -56,12 +60,17 @@ export interface Extension<P> {
export type Plugin<E extends Extension<any>> = E[];

/**
* A list of arbitrary plugins.
* From Console application perspective, a plugin is a list of extensions
* enhanced with additional data.
*/
export type PluginList = Plugin<Extension<any>>[];
export type ActivePlugin = {
name: string;
extensions: Extension<any>[];
};

// TODO(vojtech): internal code needed by plugin SDK should be moved to console-shared package

export * from './features';
export * from './models';
export * from './nav';
export * from './pages';
16 changes: 16 additions & 0 deletions frontend/packages/console-plugin-sdk/src/typings/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Extension } from '.';
import { K8sKind } from '@console/internal/module/k8s';

namespace ExtensionProperties {
export interface ModelDefinition {
models: K8sKind[];
}
}

export interface ModelDefinition extends Extension<ExtensionProperties.ModelDefinition> {
type: 'ModelDefinition';
}

export function isModelDefinition(e: Extension<any>): e is ModelDefinition {
return e.type === 'ModelDefinition';
}
Loading