diff --git a/docs/plugin-guide.md b/docs/plugin-guide.md index 578b9d49..1817b269 100644 --- a/docs/plugin-guide.md +++ b/docs/plugin-guide.md @@ -9,7 +9,7 @@ This way uses mock data and does not require other services for development. 3. Open plugin development environment `yarn dev:plugin`. Cypress UI will open in browser in a while. 4. Click `index.plugin-dev.tsx` on left side of the screen. This is will show up the plugin example. 5. Open up `plugin-api/dev/plugins/dev-plugin/index.html` with your code editor and start developing your plugin here. -> Alternatively you can make a new folder (`plugin-api/dev/plugins/new-plugin`) and add `index.html` and `manifest.json` like in dev-plugin. Also make copy of `index.plugin-dev.tsx` and update `PLUGIN_DEFINITIONS`. This is recommended so you can easily initialize this new folder as a git repo. + > Alternatively you can make a new folder (`plugin-api/dev/plugins/new-plugin`) and add `index.html` and `manifest.json` like in dev-plugin. Also make copy of `index.plugin-dev.tsx` and update `PLUGIN_DEFINITIONS`. This is recommended so you can easily initialize this new folder as a git repo. 6. Before deployment add a copy of MetaflowPluginAPI.js with the plugin and make sure that index.html refers to it correctly. 7. See `Deployment guide` from below @@ -20,14 +20,14 @@ This way requires the use of the UI application and Metaflow Metadata services. 1. Clone [Metaflow UI](https://github.com/Netflix/metaflow-ui) 2. Install dependencies `yarn install` and start up UI with `yarn start` 3. Clone [Metaflow Service](https://github.com/Netflix/metaflow-service) -4. Create a new folder to `services/ui_backend_service/plugins/installed/your-new-plugin` and add `index.html`, `manifest.json` and `MetaflowPluginAPI.js`. +4. Create a new folder to `services/ui_backend_service/plugins/installed/your-new-plugin` and add `index.html`, `manifest.json` and `MetaflowPluginAPI.js`. 5. Start up the backend service with docker-compose with [plugin configurations](https://github.com/Netflix/metaflow-service/blob/master/services/ui_backend_service/docs/plugins.md). ->`PLUGINS={"your-new-plugin": {}} docker-compose -f docker-compose.development.yml up` +> `PLUGINS={"your-new-plugin": {}} docker-compose -f docker-compose.development.yml up` 6. Run Metaflow runs with the new backend service. https://github.com/Netflix/metaflow-service -7. Open UI in browser `http://localhost:3000` and start developing the plugin. Plugin should show up in `run-header` or `task-details` depending on the manifest.json `slot` parameter. -8. See `Deployment guide` from below +7. Open UI in browser `http://localhost:3000` and start developing the plugin. Plugin should show up in `run-header`, `task-details`, or `header` depending on the manifest.json `slot` parameter. +8. See `Deployment guide` from below # Deployment @@ -36,24 +36,24 @@ Production stage plugins live on the server side. Detailed instructions at [Meta ## Way 1: GIT 1. Setup GIT repo for your plugin. -2. Configure `PLUGINS` variable at metaflow-service/ui_backend_service with following example. -> ``` -> { -> "plugin-example": "git@github.com:User/plugin-repo.git" -> } -> ``` +2. Configure `PLUGINS` variable at metaflow-service/ui_backend_service with following example. + > ``` + > { + > "plugin-example": "git@github.com:User/plugin-repo.git" + > } + > ``` or (all possible settings are described in Metaflow Service docs) ->``` +> ``` > { > "plugin-example": { > "repository": "path_to_your_repo", > "ref": "1234f5a", -> "auth": { "user": "user", "password": "password" } -> } +> "auth": { "user": "user", "password": "password" } +> } > } ->``` +> ``` 3. Start up the service and the plugin is fetched and installed. @@ -61,9 +61,9 @@ or (all possible settings are described in Metaflow Service docs) 1. Move plugin folder to `services/ui_backend_service/plugins/installed` on the backend service. 2. Configure `PLUGINS` variable at metaflow-service/ui_backend_service with -> ``` -> { -> "plugin-folder-name": {} -> } -> ``` + > ``` + > { + > "plugin-folder-name": {} + > } + > ``` 3. Start up the service diff --git a/docs/plugin-system.md b/docs/plugin-system.md index 50155db1..d52bf370 100644 --- a/docs/plugin-system.md +++ b/docs/plugin-system.md @@ -44,7 +44,11 @@ Example of basic plugin HTML: }); Metaflow.subscribeToMetadata((message) => { - console.log(`Metadata for ${resource.run_number} got updated! Metadata object for the task is ${JSON.stringify(message.data)}`); + console.log( + `Metadata for ${resource.run_number} got updated! Metadata object for the task is ${JSON.stringify( + message.data, + )}`, + ); }); }); @@ -64,17 +68,16 @@ Example of manifest.json "name": "Hello world plugin", "version": "0.0.1", "entrypoint": "plugin.html", - "slot": "task-details", + "slot": "task-details", "container": "collapsable" } ``` - This plugin is registered to be rendered in the task details section (path /FLOW_ID/RUN_NUMBER/STEP_NAME/TASK_ID in application). It subscribes to a custom event "customEvent" and is prepared to send another custom event "customEvent" on button click. It also subscribes for updates about task Metadata. ## Plugin slots -There are two implemented plugin slots, `run-header` and `task-details`. The desired slot must be defined in manifest.json file. +There are three implemented plugin slots, `run-header`, `task-details`, and `header`. The desired slot must be defined in manifest.json file. ### run-header @@ -84,6 +87,10 @@ The `run-header` plugin will be rendered below run details in a collapsable elem The `task-details` plugin will be rendered below task details in a collapsable element. +### header + +The `header` plugin will be rendered below the Metaflow logo in the header. + ## Plugin configurations Plugins can be given custom parameters from the server-side. These parameters will be passed to the plugin with the `onReady` callback call. diff --git a/plugin-api/README.md b/plugin-api/README.md index e0ac3210..aa2e2ab9 100644 --- a/plugin-api/README.md +++ b/plugin-api/README.md @@ -6,17 +6,17 @@ This folder contains the JS API for making UI plugins for Metaflow UI. There are Plugins will use JS API to communicate with the host application. Plugins must call at least the `register` function from the API to get rendered. -| function | type | description | -| -- | -- | -- | -| onReady | (callback: (configuration, resource) => void) => void | Register callback function to be run when host and plugin iframe are ready. | +| function | type | description | +| ---------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| onReady | (callback: (configuration, resource) => void) => void | Register callback function to be run when host and plugin iframe are ready. | | subscribe | (paths: string[], cb: (message: { path: string, data: any }) => void) => void | Subscribe to contextual data updates from application. Possible paths: 'metadata' or 'run-metadata' | -| subscribeToMetadata | (callback: (message: Record) => void) => void | Subscribe to task metadata | -| subscribeToRunMetadata | (callback: (message: Record) => void) => void | Subscribe to run metadata | -| on | (events: string[], cb: (message: { type: string, data: any }) => void) => void | Subscribe to any event by event string. | -| call | (event: string, data: any) => void | Call any custom event with string and any data. | -| sendNotification | (message: string \| { type: string, message: string }) => void | Call notification API from host application. | -| setHeight | (height: number \| undefined) => void | Update height of iframe container for plugin. | -| setVisibility | (visibility: boolean) => void | Update visibility of the plugin. Note that if will stay in iframe even if visibility is set false. | +| subscribeToMetadata | (callback: (message: Record) => void) => void | Subscribe to task metadata | +| subscribeToRunMetadata | (callback: (message: Record) => void) => void | Subscribe to run metadata | +| on | (events: string[], cb: (message: { type: string, data: any }) => void) => void | Subscribe to any event by event string. | +| call | (event: string, data: any) => void | Call any custom event with string and any data. | +| sendNotification | (message: string \| { type: string, message: string }) => void | Call notification API from host application. | +| setHeight | (height: number \| undefined) => void | Update height of iframe container for plugin. | +| setVisibility | (visibility: boolean) => void | Update visibility of the plugin. Note that if will stay in iframe even if visibility is set false. | ## How to @@ -48,14 +48,13 @@ Plugins requires some configuration to their manifest.json ```json { - "name": "Plugin name", // Name will be visible in UI + "name": "Plugin name", // Name will be visible in UI "version": "0.0.1", - "entrypoint": "index.html", // Name for HTML file used - "slot": "run-header", // Slot in UI where plugin is rendered. "run-header" or "task-details" - "visible": true, // (Optional) Define if plugin should be visible by default. Default: true - "container": "titled-container", // (Optional) Define what kind of container is used for plugin. "collapsable" or "titled-container". Default: "collapsable" - "containerProps": {}, // (Optional) Properties for container element. For example collapsable can take { "initialState": true } to be open by default. Default: null - "useApplicationStyles": false, // (Optional) Disable injecting basic styles from main application. Default: true - + "entrypoint": "index.html", // Name for HTML file used + "slot": "run-header", // Slot in UI where plugin is rendered. "run-header", "task-details", or "header" + "visible": true, // (Optional) Define if plugin should be visible by default. Default: true + "container": "titled-container", // (Optional) Define what kind of container is used for plugin. "collapsable" or "titled-container". Default: "collapsable" + "containerProps": {}, // (Optional) Properties for container element. For example collapsable can take { "initialState": true } to be open by default. Default: null + "useApplicationStyles": false // (Optional) Disable injecting basic styles from main application. Default: true } -``` \ No newline at end of file +``` diff --git a/src/components/AppBar/__tests__/AppBar.test.cypress.tsx b/src/components/AppBar/__tests__/AppBar.test.cypress.tsx index acf6e05c..6da104c5 100644 --- a/src/components/AppBar/__tests__/AppBar.test.cypress.tsx +++ b/src/components/AppBar/__tests__/AppBar.test.cypress.tsx @@ -5,17 +5,20 @@ import { BrowserRouter as Router, Route } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; import theme from '../../../theme'; import AppBar from '..'; +import { PluginsProvider } from '../../Plugins/PluginManager'; describe('AppBar test', () => { it('AppBar basic', () => { cy.viewport(1000, 600); mount( - - - - - + + + + + + + , ); // test all of the AppBars child components render diff --git a/src/components/AppBar/index.tsx b/src/components/AppBar/index.tsx index aa43e9a9..fca10ef5 100755 --- a/src/components/AppBar/index.tsx +++ b/src/components/AppBar/index.tsx @@ -6,6 +6,7 @@ import Breadcrumb from '../Breadcrumb'; import { ItemRow } from '../Structure'; import HelpMenu from '../HelpMenu'; import ConnectionStatus from '../ConnectionStatus'; +import PluginGroup from '../Plugins/PluginGroup'; // // Main application bar which is always shown on top of the page @@ -24,6 +25,9 @@ const AppBar: React.FC = () => { + + + ); }; @@ -39,15 +43,16 @@ const Wrapper = styled.header` display: flex; align-items: center; justify-content: flex-start; - position: fixed; + position: sticky; top: 0; left: 0; right: 0; - height: ${(p) => p.theme.layout.appbarHeight}rem; + min-height: ${(p) => p.theme.layout.appbarHeight}rem; margin: 0 auto; padding: 0 ${(p) => p.theme.layout.pagePaddingX}rem; background: ${(p) => p.theme.color.bg.white}; z-index: 999; + flex-direction: column; `; const Logo = styled.img` @@ -59,9 +64,8 @@ const LogoLink = styled(Link)` `; const ConnectionStatusWrapper = styled.div` - position: absolute; - top: 0; - right: 0; - padding: 0 ${(p) => p.theme.layout.pagePaddingX}rem; + display: flex; + justify-content: flex-end; + width: 100%; padding-top: ${(p) => p.theme.spacer.md}rem; `; diff --git a/src/components/ConnectionStatus/index.tsx b/src/components/ConnectionStatus/index.tsx index a7612a76..e9b747a4 100644 --- a/src/components/ConnectionStatus/index.tsx +++ b/src/components/ConnectionStatus/index.tsx @@ -99,6 +99,7 @@ const Wrapper = styled.div<{ status: RealtimeStatus }>` &:hover ${Text} { opacity: 1; } + height: 2rem; `; const StatusColorIndicator = styled.div<{ status: RealtimeStatus }>` diff --git a/src/components/Plugins/PluginGroup.tsx b/src/components/Plugins/PluginGroup.tsx index d62c6768..29b8ea7f 100644 --- a/src/components/Plugins/PluginGroup.tsx +++ b/src/components/Plugins/PluginGroup.tsx @@ -19,7 +19,7 @@ type Props = { resourceParams?: Record; }; -const VALID_CONTAINERS = ['collapsable', 'titled-container']; +const VALID_CONTAINERS = ['collapsable', 'titled-container', 'none']; // // Renders list of plugin to iframe in collapsable element. diff --git a/src/components/Plugins/PluginManager.tsx b/src/components/Plugins/PluginManager.tsx index 94d2c32e..a825fc8c 100644 --- a/src/components/Plugins/PluginManager.tsx +++ b/src/components/Plugins/PluginManager.tsx @@ -44,11 +44,11 @@ type PluginVersionInfo = { // Constants. Plugin will not render if it doesn't satisfy SUPPORTED_PLUGIN_API_VERSION. // -export type AllowedSlot = 'task-details' | 'run-header'; +export type AllowedSlot = 'task-details' | 'run-header' | 'header'; const SUPPORTED_PLUGIN_API_VERSION = '0.13.0'; const RECOMMENDED_PLUGIN_API_VERSION = '1.1.0'; -const ALLOWED_SLOTS: AllowedSlot[] = ['task-details', 'run-header']; +const ALLOWED_SLOTS: AllowedSlot[] = ['task-details', 'run-header', 'header']; // // Utils diff --git a/src/components/Structure/index.tsx b/src/components/Structure/index.tsx index d722cea2..5a4ce0a1 100755 --- a/src/components/Structure/index.tsx +++ b/src/components/Structure/index.tsx @@ -11,12 +11,6 @@ export const Page = styled.div` min-height: 100vh; display: flex; flex-direction: column; - - &:before { - display: block; - content: ''; - height: ${(p) => p.theme.layout.appbarHeight}rem; - } `; export const Section = styled.section`