Skip to content

Commit

Permalink
Support models declared by plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechszocs committed May 27, 2019
1 parent efd2324 commit 397eaaa
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 69 deletions.
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 {
/**
* 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

0 comments on commit 397eaaa

Please sign in to comment.