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
90 changes: 89 additions & 1 deletion docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- [Object field](#object-field)
- [Restrictions](#restrictions)
- [Json field](#json-field)

- [Hooks](#hooks)

Each plugin should be created handle a specific part of the data.
Avoid creating a single plugin to handle all the data, as this will make the code harder to maintain and understand.
Expand Down Expand Up @@ -389,3 +389,91 @@ The `json` field is an escape-hatch that allows the user to input any JSON data.
```

![JSON](images/field-json.png)

## Hooks

> [!WARNING]
> The hooks are an advanced feature and should be used with caution.

Hooks are functions that can be added to the plugins to perform actions at specific points in their lifecycle.

Available hooks:
- `onAfterInit(targetInstance: Plugin, data: any) => void` - Executed after the target plugin's `init` function. Can be a promise.
- `onAfterEditSchema(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => PluginEditSchema` - Allows changing a plugin's schema. Receives the schema returned by the target plugin's `editSchema` function and the data entered in the form. Should return the modified schema.

Hooks should be registered in the plugin's constructor:

```ts
import { Plugin } from '@stac-manager/data-core';

export class PluginName extends Plugin {
name = 'Plugin Name';

constructor() {
super();
this.registerHook(
'Target plugin name',
'onAfterInit',
(targetInstance: Plugin, data: any) => {
// Do something.
}
);

this.registerHook(
'Target plugin name',
'onAfterEditSchema',
(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => {
// Do something.
return schema;
}
);
}
}
```

**Real world example**
Whenever an STAC extension plugin is added, it should add the extension to the `stac_extensions` field of the CollectionsCore plugin. This will allow the interface to show the extension as an option in the dropdown.

This can be achieved with th `onAfterEditSchema` hook:
```ts
import { Plugin } from '@stac-manager/data-core';

export class PluginName extends Plugin {
name = 'Plugin Name';

constructor() {
super();
this.registerHook(
'CollectionsCore',
'onAfterEditSchema',
(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => {
const stac_extensions = schema.properties
.stac_extensions as SchemaFieldArray<SchemaFieldString>;

// Set the new extension value in the schema.
stac_extensions.items.enum!.push([value, label]);

return schema;
}
);
}
}
```

> [!TIP]
> Given that this is a common enough use-case there is a helper function (`addStacExtensionOption`) that can be used.
> ```ts
>import { Plugin } from '@stac-manager/data-core';
>import { addStacExtensionOption } from '@stac-manager/data-plugins';
>
>class PluginName extends Plugin {
> constructor() {
> super();
>
> addStacExtensionOption(
> this,
> 'Item Assets Definition',
> 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json'
> );
> }
>}
16 changes: 11 additions & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ STAC-Manager is a react app designed for managing the values of a STAC (SpatioTe
The ecosystem is composed of a web app (the client) and a plugin system that powers it.
The different parts of the project are located in the `packages` directory structured as follows:

- [`@stac-manager/client`](./packages/client) - STAC-Manager web app.
- [`@stac-manager/data-core`](./packages/data-core) - Core functionality of the form builder plugin system.
- [`@stac-manager/data-widgets`](./packages/data-widgets) - Form components to be used by the form builder plugin system, when custom ones are not provided.
- [`@stac-manager/data-plugins`](./packages/data-plugins) - Data plugins for the forms. Each plugin defines how a section of the data structure is displayed and edited.
- [`@stac-manager/client`](../packages/client) - STAC-Manager web app.
- [`@stac-manager/data-core`](../packages/data-core) - Core functionality of the form builder plugin system.
- [`@stac-manager/data-widgets`](../packages/data-widgets) - Form components to be used by the form builder plugin system, when custom ones are not provided.
- [`@stac-manager/data-plugins`](../packages/data-plugins) - Data plugins for the forms. Each plugin defines how a section of the data structure is displayed and edited.

The `@stac-manager/data-*` packages contain the default implementation of the plugin system, the widgets used to render the forms and some core functions to allow the system to be extended.

Expand All @@ -22,12 +22,18 @@ Each plugin handles a specific part of the data and is responsible for defining

## Configuration

### Client

See the client-specific instructions, such as configuring the server's STAC API, in the [README of the client package](../packages/client#client-specific-instructions).

### Plugins

STAC-Manager's [config file](/packages/client/src/plugin-system/config.ts) specifies the plugins that the app uses for Collections and Items while extending the `Default Plugin Widget Configuration` which defines the widgets for the basic field types.
_See the [data-widgets/config](/packages/data-widgets/lib/config/index.ts) for a list of existent widgets._

When creating a new plugin or a new widget, the configuration should be updated with the new plugin/widget.

### Example config
#### Example config

The config object should contain a list of plugins to use for the collections and items, as well as the widget configuration (which widgets to use for which field types).

Expand Down
10 changes: 10 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ See root README.md for instructions on how to install and run the project.

Some client options are controlled by environment variables. These are:
```
# App config
## Title and description of the app for metadata
APP_TITLE
APP_DESCRIPTION

# API
## If the app is being served in from a subfolder, the domain url must be set.
PUBLIC_URL
REACT_APP_STAC_BROWSER
REACT_APP_STAC_API

# Theming
REACT_APP_THEME_PRIMARY_COLOR
REACT_APP_THEME_SECONDARY_COLOR
```

You must provide a value for the `REACT_APP_STAC_API` environment variable. This should be the URL of the STAC API you wish to interact with.
Expand Down
2 changes: 1 addition & 1 deletion packages/data-core/lib/context/plugin-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const PluginConfigContext = createContext<PluginConfigContextProps | null>(

/**
* Global context provider for the configuration
*
*
* @param props.config Configuration object
* @param props.children Child components
*/
Expand Down
47 changes: 46 additions & 1 deletion packages/data-core/lib/plugin-utils/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,31 @@ export type PluginEditSchema =
| undefined
| symbol;

export interface PluginHook {
// Name of the target plugin.
name: string;

// The `onAfterInit` hook is executed after the target plugin's `init`
// function.
onAfterInit?: (pluginInstance: Plugin, data: any) => void;

// The `onAfterEditSchema` hook is composed with the target plugin's
// `editSchema` function.
onAfterEditSchema?: (
pluginInstance: Plugin,
formData: any,
schema: PluginEditSchema
) => PluginEditSchema;
}

const HIDDEN: unique symbol = Symbol('hidden');
const HOOKS: unique symbol = Symbol('hooks');

export abstract class Plugin {
static HIDDEN = Symbol('hidden');
static readonly HIDDEN: typeof HIDDEN = HIDDEN;
static readonly HOOKS: typeof HOOKS = HOOKS;

[HOOKS]: PluginHook[] = [];

name: string = 'Plugin';

Expand All @@ -25,6 +48,28 @@ export abstract class Plugin {
exitData(data: any): Record<string, any> {
throw new Error(`Plugin [${this.name}] must implement exitData`);
}

/**
* Registers a hook to be applied on a given plugin.
*
* @param targetName - The name of the target plugin to which the hook will be
* applied.
* @param hookName - The name of the hook.
* @param hook - The hook function.
*/
registerHook<K extends keyof Omit<PluginHook, 'name'>>(
targetName: string,
hookName: K,
hook: PluginHook[K]
) {
const hookEntry = this[Plugin.HOOKS].find((h) => h.name === targetName);

if (hookEntry) {
hookEntry[hookName] = hook;
} else {
this[Plugin.HOOKS].push({ name: targetName, [hookName]: hook });
}
}
}

export type PluginConfigResolved = Plugin | Plugin[] | undefined | null;
Expand Down
92 changes: 91 additions & 1 deletion packages/data-core/lib/plugin-utils/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { cloneDeep } from 'lodash-es';
import { Plugin, PluginConfigItem } from './plugin';

/**
* Resolves the plugin config.
*
* @param plugins - An array of plugin configuration items which can be
* instances of `Plugin` or functions that return a `Plugin`.
* @param data - The data to be passed to plugin functions if they are not
* instances of `Plugin`.
* @returns An array of processed `Plugin` instances after applying hooks.
*/
export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
return plugins
const p = plugins
.flatMap((pl) => {
if (pl instanceof Plugin) {
return pl;
Expand All @@ -11,4 +21,84 @@ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => {
return;
})
.filter((p) => p instanceof Plugin);

return applyHooks(p);
};

/**
* Applies hooks from the provided plugins to their respective targets.
*
* This function iterates over each plugin and applies hooks such as
* `onAfterInit` and `onAfterEditSchema` to the corresponding target plugins.
* The hooks are executed in the context of the source plugin.
*
* @param plugins - List of plugins. All the source and target plugins must be
* on the list.
*
*
* @remarks
* - The `onAfterInit` hook is executed after the target plugin's `init`
* function.
* - The `onAfterEditSchema` hook is composed with the target plugin's
* `editSchema` function.
*
* @example
* ```typescript
* class MyPlugin extends Plugin {
* name = 'MyPlugin';
*
* [Plugin.HOOKS]: [
* {
* name: 'pluginA', // Target plugin
* onAfterInit: async (targetInstance, data) => { }, // Executes after pluginA's init function.
* onAfterEditSchema: (targetInstance, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one.
* },
* {
* name: 'pluginB', // Target plugin
* onAfterInit: async (targetInstance, data) => { }, // Executes after pluginB's init function.
* }
* ];
* }
*
* applyHooks(plugins);
* ```
*/
export const applyHooks = (plugins: Plugin[]) => {
const pluginsCopy = cloneDeep(plugins);

for (const plSource of pluginsCopy) {
for (const hook of plSource[Plugin.HOOKS]) {
// Target where to apply the hook
const plTarget = plugins.find((p) => p.name === hook.name);
if (!plTarget) {
continue;
}

// The onAfterInit hook is made by executing one function after another.
if (hook.onAfterInit) {
const fn = hook.onAfterInit;
const origInit = plTarget.init;
plTarget.init = async (data: any) => {
await origInit.call(plTarget, data);
await fn.call(plSource, plTarget, data);
};
}

// The onAfterEditSchema hook is made by composing functions.
if (hook.onAfterEditSchema) {
const fn = hook.onAfterEditSchema;
const origEditSchema = plTarget.editSchema;
plTarget.editSchema = (formData?: any) => {
return fn.call(
plSource,
plTarget,
formData,
origEditSchema.call(plTarget, formData)
);
};
}
}
}

return pluginsCopy;
};
11 changes: 1 addition & 10 deletions packages/data-plugins/lib/collections/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,7 @@ export class PluginCore extends Plugin {
allowOther: {
type: 'string'
},
enum: [
[
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json',
'Item Assets Defenition'
],
[
'https://stac-extensions.github.io/render/v2.0.0/schema.json',
'Render'
]
]
enum: []
}
},
spatial: {
Expand Down
19 changes: 17 additions & 2 deletions packages/data-plugins/lib/collections/ext-item-assets.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { Plugin, PluginEditSchema } from '@stac-manager/data-core';
import { array2Object, hasExtension, object2Array } from '../utils';
import {
addStacExtensionOption,
array2Object,
hasStacExtension,
object2Array
} from '../utils';

export class PluginItemAssets extends Plugin {
name = 'Item Assets Extension';

constructor() {
super();

addStacExtensionOption(
this,
'Item Assets Definition',
'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json'
);
}

editSchema(data: any): PluginEditSchema {
if (!hasExtension(data, 'item-assets')) {
if (!hasStacExtension(data, 'item-assets')) {
return Plugin.HIDDEN;
}

Expand Down
Loading