Skip to content

Commit

Permalink
Merge pull request #7023 from rohitkrai03/catalog-extensibility
Browse files Browse the repository at this point in the history
Add new catalog extensions and catalog service provider
  • Loading branch information
openshift-merge-robot committed Nov 11, 2020
2 parents 4d4c2c3 + 1cf8944 commit 18e98ac
Show file tree
Hide file tree
Showing 22 changed files with 1,137 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const useResolvedExtensions = <E extends Extension>(
return resolvedExtensions;
};

type ResolvedExtension<E extends Extension, P = ExtensionProperties<E>> = UpdateExtensionProperties<
LoadedExtension<E>,
ResolvedCodeRefProperties<P>
>;
export type ResolvedExtension<
E extends Extension,
P = ExtensionProperties<E>
> = UpdateExtensionProperties<LoadedExtension<E>, ResolvedCodeRefProperties<P>>;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { executeReferencedFunction } from '../coderef-utils';
import { codeRefSymbol } from '../coderef-resolver';
import { executeReferencedFunction, getExecutableCodeRef } from '../coderef-utils';

describe('executeReferencedFunction', () => {
it('executes the referenced function with given args and returns its result', async () => {
Expand Down Expand Up @@ -37,3 +38,12 @@ describe('executeReferencedFunction', () => {
expect(result).toBe(null);
});
});

describe('getExecutableCodeRef', () => {
it('should add codeRefSymbol to the codeRef to make it executable', () => {
const codeRef = jest.fn(() => Promise.resolve({}));
expect(codeRef[codeRefSymbol]).toBeFalsy();
const executableCodeRef = getExecutableCodeRef(codeRef);
expect(executableCodeRef[codeRefSymbol]).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-console */

import { CodeRef } from '../types';
import { codeRefSymbol } from './coderef-resolver';

/**
* Execute function referenced by the `CodeRef` with given arguments.
Expand All @@ -21,3 +22,17 @@ export const executeReferencedFunction = async <T extends (...args: any[]) => an
return null;
}
};

/**
* Convert any codeRef to an executable codeRef that can be executed by useResolvedExtension.
*
* Adds codeRefSymbol to the codeRef.
*
* TODO: Remove this once https://github.com/openshift/console/pull/7163 gets merged that adds support for dynamic extensions in static plugins.
*
* @param ref codeRef that needs to be converted to an executable codeRef.
*/
export const getExecutableCodeRef = (ref: CodeRef): CodeRef => {
ref[codeRefSymbol] = true;
return ref;
};
7 changes: 7 additions & 0 deletions frontend/packages/console-plugin-sdk/src/typings/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export type ExtensionTypeGuard<E extends Extension> = (e: E) => e is E;
*/
export type LazyLoader<T extends {} = {}> = () => Promise<React.ComponentType<Partial<T>>>;

/**
* Code reference, resolved to a function that returns the object `T`.
*
* TODO: Remove this once https://github.com/openshift/console/pull/7163 gets merged that adds support for dynamic extensions in static plugins.
*/
export type CodeRef<T> = () => Promise<T>;

/**
* From Console application perspective, a plugin is a list of extensions
* enhanced with additional data.
Expand Down
97 changes: 96 additions & 1 deletion frontend/packages/console-plugin-sdk/src/typings/dev-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,112 @@
import { K8sKind, K8sResourceKind } from '@console/internal/module/k8s';
import { Extension } from './base';
import { CodeRef, Extension } from './base';

namespace ExtensionProperties {
export interface DevCatalogModel {
model: K8sKind;
normalize: (data: K8sResourceKind[]) => K8sResourceKind[];
}

export interface CatalogItemType {
/** Type for the catalog item. */
type: string;
/** Title fpr the catalog item. */
title: string;
/** Description for the type specific catalog. */
catalogDescription: string;
/** Description for the catalog item type. */
typeDescription: string;
/** Custom filters specific to the catalog item */
filters?: CatalogFilter[];
/** Custom groupings specific to the catalog item */
groupings?: CatalogGrouping[];
}

export interface CatalogItemProvider {
/** Type ID for the catalog item type. */
type: string;
/** Fetch items and normalize it for the catalog. Value is a react effect hook. */
provider: CodeRef<CatalogExtensionHook<CatalogItem[]>>;
}
}

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

export interface CatalogItemType extends Extension<ExtensionProperties.CatalogItemType> {
type: 'Catalog/ItemType';
}

export interface CatalogItemProvider extends Extension<ExtensionProperties.CatalogItemProvider> {
type: 'Catalog/ItemProvider';
}

export const isDevCatalogModel = (e: Extension): e is DevCatalogModel => {
return e.type === 'DevCatalogModel';
};

export const isCatalogItemType = (e: Extension): e is CatalogItemType => {
return e.type === 'Catalog/ItemType';
};

export const isCatalogItemProvider = (e: Extension): e is CatalogItemProvider => {
return e.type === 'Catalog/ItemProvider';
};

export type CatalogExtensionHookResult<T> = [T, boolean, any];

export type CatalogExtensionHookOptions = {
namespace: string;
};

export type CatalogExtensionHook<T> = (
options: CatalogExtensionHookOptions,
) => CatalogExtensionHookResult<T>;

export type CatalogItem = {
uid: string;
type: string;
name: string;
provider?: string;
description?: string;
tags?: string[];
creationTimestamp?: string;
supportUrl?: string;
documentationUrl?: string;
attributes?: {
[key: string]: string;
};
cta: {
label: string;
href: string;
};
icon?: {
url?: string;
class?: string;
};
details?: {
properties?: CatalogItemDetailsProperty[];
descriptions?: CatalogItemDetailsDescription[];
};
};

export type CatalogItemDetailsProperty = {
label: string;
value: string | React.ReactNode;
};

export type CatalogItemDetailsDescription = {
label?: string;
value: string | React.ReactNode;
};

export type CatalogFilter = {
label: string;
attribute: string;
};

export type CatalogGrouping = {
label: string;
attribute: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { PageHeading, skeletonCatalog, StatusBox } from '@console/internal/components/utils';
import { CatalogService } from './service/CatalogServiceProvider';

type CatalogControllerProps = CatalogService;

const CatalogController: React.FC<CatalogControllerProps> = ({
type,
items,
loaded,
loadError,
catalogExtensions,
}) => {
let title = 'Developer Catalog';
let description =
'Add shared applications, services, event sources, or source-to-image builders to your project from the Developer catalog. Cluster administrators can customize the content made available in the catalog.';

if (type && catalogExtensions.length > 0) {
title = catalogExtensions[0].properties.title;
description = catalogExtensions[0].properties.catalogDescription;
}

return (
<>
<div className="co-m-page__body">
<div className="co-catalog">
<PageHeading title={title} />
<p className="co-catalog-page__description">{description}</p>
<div className="co-catalog__body">
<StatusBox
skeleton={skeletonCatalog}
data={items}
loaded={loaded}
loadError={loadError}
label="Catalog items"
>
<h2>Loaded Catalog Items - {items.length}</h2>
</StatusBox>
</div>
</div>
</div>
</>
);
};

export default CatalogController;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import Helmet from 'react-helmet';
import { RouteComponentProps } from 'react-router';
import { getQueryArgument } from '@console/internal/components/utils';
import CreateProjectListPage from '../projects/CreateProjectListPage';
import NamespacedPage, { NamespacedPageVariants } from '../NamespacedPage';
import CatalogServiceProvider from './service/CatalogServiceProvider';
import CatalogController from './CatalogController';

type CatalogPageProps = RouteComponentProps<{
ns?: string;
}>;

const CatalogPage: React.FC<CatalogPageProps> = ({ match }) => {
const catalogType = getQueryArgument('catalogType');
const namespace = match.params.ns;

return (
<NamespacedPage variant={NamespacedPageVariants.light} hideApplications>
<Helmet>
<title>Developer Catalog</title>
</Helmet>
{namespace ? (
<CatalogServiceProvider namespace={namespace} catalogType={catalogType}>
{(service) => <CatalogController {...service} />}
</CatalogServiceProvider>
) : (
<CreateProjectListPage title="Developer Catalog">
Select a project to view the Developer Catalog
</CreateProjectListPage>
)}
</NamespacedPage>
);
};

export default CatalogPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { CatalogItemProvider, CatalogItemType, Plugin } from '@console/plugin-sdk';
import { builderImageProvider, helmChartProvider, templateProvider } from './providers';

export type CatalogConsumedExtensions = CatalogItemProvider | CatalogItemType;

export const catalogPlugin: Plugin<CatalogConsumedExtensions> = [
{
type: 'Catalog/ItemType',
properties: {
type: 'BuilderImage',
title: 'Builder Images',
catalogDescription:
'Browse for container images that support a particular language or framework. Cluster administrators can customize the content made available in the catalog.',
typeDescription:
'Builder images are container images that build source code for a particular language or framework.',
},
flags: {
required: ['OPENSHIFT'],
},
},
{
type: 'Catalog/ItemProvider',
properties: {
type: 'BuilderImage',
provider: builderImageProvider,
},
flags: {
required: ['OPENSHIFT'],
},
},
{
type: 'Catalog/ItemType',
properties: {
type: 'Template',
title: 'Templates',
catalogDescription:
'Browse for templates that can deploy services, create builds, or create any resources the template enables. Cluster administrators can customize the content made available in the catalog.',
typeDescription:
'Templates are sets of objects for creating services, build configurations, and anything you have permission to create within a project.',
},
flags: {
required: ['OPENSHIFT'],
},
},
{
type: 'Catalog/ItemProvider',
properties: {
type: 'Template',
provider: templateProvider,
},
flags: {
required: ['OPENSHIFT'],
},
},
{
type: 'Catalog/ItemType',
properties: {
type: 'HelmChart',
title: 'Helm Charts',
catalogDescription:
'Browse for charts that help manage complex installations and upgrades. Cluster administrators can customize the content made available in the catalog.',
typeDescription:
'Helm charts are packages for deploying an application or components of a larger application.',
filters: [
{
label: 'Chart Repositories',
attribute: 'chartRepositoryName',
},
],
},
},
{
type: 'Catalog/ItemProvider',
properties: {
type: 'HelmChart',
provider: helmChartProvider,
},
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
CatalogItemProvider,
CatalogItemType,
isCatalogItemProvider,
isCatalogItemType,
} from '@console/plugin-sdk';
import {
ResolvedExtension,
useResolvedExtensions,
} from '@console/dynamic-plugin-sdk/src/api/useResolvedExtensions';

const useCatalogExtensions = (
catalogType: string,
): [ResolvedExtension<CatalogItemType>[], ResolvedExtension<CatalogItemProvider>[]] => {
const itemTypeExtensions = useResolvedExtensions<CatalogItemType>(isCatalogItemType);
const itemProviderExtensions = useResolvedExtensions<CatalogItemProvider>(isCatalogItemProvider);

const catalogTypeExtensions = catalogType
? itemTypeExtensions.filter((e) => e.properties.type === catalogType)
: itemTypeExtensions;

const catalogProviderExtensions = catalogType
? itemProviderExtensions.filter((e) => e.properties.type === catalogType)
: itemProviderExtensions;

return [catalogTypeExtensions, catalogProviderExtensions];
};

export default useCatalogExtensions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getExecutableCodeRef } from '@console/dynamic-plugin-sdk/src/coderefs/coderef-utils';

export const builderImageProvider = getExecutableCodeRef(() =>
import('./useBuilderImages' /* webpackChunkName: "builder-image-provider" */).then(
(m) => m.default,
),
);

export const templateProvider = getExecutableCodeRef(() =>
import('./useTemplates' /* webpackChunkName: "template-provider" */).then((m) => m.default),
);

export const helmChartProvider = getExecutableCodeRef(() =>
import('./useHelmCharts' /* webpackChunkName: "helm-charts-provider" */).then((m) => m.default),
);

0 comments on commit 18e98ac

Please sign in to comment.