diff --git a/docs/developer-guide/extensions/ui-extensions.md b/docs/developer-guide/extensions/ui-extensions.md index 8d3d9dc4a3882..ffe4ba936cc74 100644 --- a/docs/developer-guide/extensions/ui-extensions.md +++ b/docs/developer-guide/extensions/ui-extensions.md @@ -6,7 +6,7 @@ in the `argocd-server` Pods that are placed in the `/tmp/extensions` directory a ``` /tmp/extensions ├── example1 -│   └── extension-1.js +│ └── extension-1.js └── example2 └── extension-2.js ``` @@ -73,7 +73,7 @@ registerSystemLevelExtension(component: ExtensionComponent, title: string, optio Below is an example of a simple system level extension: -```typescript +```javascript ((window) => { const component = () => { return React.createElement( @@ -106,7 +106,7 @@ registerStatusPanelExtension(component: StatusPanelExtensionComponent, title: st Below is an example of a simple extension: -```typescript +```javascript ((window) => { const component = () => { return React.createElement( @@ -129,32 +129,95 @@ It is also possible to add an optional flyout widget to your extension. It can b Below is an example of an extension using the flyout widget: -```typescript + +```javascript ((window) => { const component = (props: { - openFlyout: () => any - }) => { + openFlyout: () => any + }) => { return React.createElement( - "div", - { - style: { padding: "10px" }, - onClick: () => props.openFlyout() - }, - "Hello World" + "div", + { + style: { padding: "10px" }, + onClick: () => props.openFlyout() + }, + "Hello World" ); }; const flyout = () => { return React.createElement( - "div", - { style: { padding: "10px" } }, - "This is a flyout" + "div", + { style: { padding: "10px" } }, + "This is a flyout" ); }; window.extensionsAPI.registerStatusPanelExtension( - component, - "My Extension", - "my_extension", - flyout + component, + "My Extension", + "my_extension", + flyout ); })(window); ``` + +## Top Bar Action Menu Extensions + +The top bar panel is the action menu at the top of the application view where the action buttons are displayed like Details, Sync, Refresh. Argo CD allows you to add new button to the top bar action menu of an application. +When the extension button is clicked, the custom widget will be rendered in a flyout panel. + +The extension should be registered using the `extensionsAPI.registerTopBarActionMenuExt` method: + +```typescript +registerTopBarActionMenuExt( + component: TopBarActionMenuExtComponent, + title: string, + id: string, + flyout?: ExtensionComponent, + shouldDisplay: (app?: Application) => boolean = () => true, + iconClassName?: string, + isMiddle = false +) +``` + +The callback function `shouldDisplay` should return true if the extension should be displayed and false otherwise: + +```typescript +const shouldDisplay = (app: Application) => { + return application.metadata?.labels?.['application.environmentLabelKey'] === "prd"; +}; +``` + +Below is an example of a simple extension with a flyout widget: + +```javascript +((window) => { + const shouldDisplay = () => { + return true; + }; + const flyout = () => { + return React.createElement( + "div", + { style: { padding: "10px" } }, + "This is a flyout" + ); + }; + const component = () => { + return React.createElement( + "div", + { + onClick: () => flyout() + }, + "Toolbar Extension Test" + ); + }; + window.extensionsAPI.registerTopBarActionMenuExt( + component, + "Toolbar Extension Test", + "Toolbar_Extension_Test", + flyout, + shouldDisplay, + '', + true + ); +})(window); +``` \ No newline at end of file diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index c300a2bc24a2e..050ed358a5e20 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -30,7 +30,7 @@ import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown import {useSidebarTarget} from '../../../sidebar/sidebar'; import './application-details.scss'; -import {AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service'; +import {TopBarActionMenuExt, AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service'; interface ApplicationDetailsState { page: number; @@ -44,6 +44,8 @@ interface ApplicationDetailsState { extensionsMap?: {[key: string]: AppViewExtension}; statusExtensions?: StatusPanelExtension[]; statusExtensionsMap?: {[key: string]: StatusPanelExtension}; + topBarActionMenuExts?: TopBarActionMenuExt[]; + topBarActionMenuExtsMap?: {[key: string]: TopBarActionMenuExt}; } interface FilterInput { @@ -94,6 +96,11 @@ export class ApplicationDetails extends React.Component { statusExtensionsMap[ext.id] = ext; }); + const topBarActionMenuExts = services.extensions.getActionMenuExtensions(); + const topBarActionMenuExtsMap: {[key: string]: TopBarActionMenuExt} = {}; + topBarActionMenuExts.forEach(ext => { + topBarActionMenuExtsMap[ext.id] = ext; + }); this.state = { page: 0, groupedResources: [], @@ -104,7 +111,9 @@ export class ApplicationDetails extends React.Component @@ -580,7 +590,14 @@ export class ApplicationDetails extends React.Component} ], - actionMenu: {items: this.getApplicationActionMenu(application, true)}, + actionMenu: { + items: [ + ...this.getApplicationActionMenu(application, true), + ...(this.state.topBarActionMenuExts + ?.filter(ext => ext.shouldDisplay?.(application)) + .map(ext => this.renderActionMenuItem(ext, tree, application, this.setExtensionPanelVisible)) || []) + ] + }, tools: (
@@ -866,10 +883,16 @@ export class ApplicationDetails extends React.Component this.setExtensionPanelVisible('')}> + {this.selectedExtension !== '' && activeStatusExt?.flyout && } + + this.setExtensionPanelVisible('')}> - {this.selectedExtension !== '' && activeExtension && activeExtension.flyout && ( - + {this.selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && ( + )} @@ -881,7 +904,13 @@ export class ApplicationDetails extends React.Component ); } - + private renderActionMenuItem(ext: TopBarActionMenuExt, tree: appModels.ApplicationTree, application: appModels.Application, showExtension?: (id: string) => any): any { + return { + action: () => this.setExtensionPanelVisible(ext.id), + title: showExtension && showExtension(ext.id)} />, + iconClassName: ext.iconClassName + }; + } private getApplicationActionMenu(app: appModels.Application, needOverlapLabelOnNarrowScreen: boolean) { const refreshing = app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]; const fullName = AppUtils.nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); diff --git a/ui/src/app/shared/services/extensions-service.ts b/ui/src/app/shared/services/extensions-service.ts index eff46514e3046..887b767df3305 100644 --- a/ui/src/app/shared/services/extensions-service.ts +++ b/ui/src/app/shared/services/extensions-service.ts @@ -3,8 +3,8 @@ import * as minimatch from 'minimatch'; import {Application, ApplicationTree, State} from '../models'; -type ExtensionsEventType = 'resource' | 'systemLevel' | 'appView' | 'statusPanel'; -type ExtensionsType = ResourceTabExtension | SystemLevelExtension | AppViewExtension | StatusPanelExtension; +type ExtensionsEventType = 'resource' | 'systemLevel' | 'appView' | 'statusPanel' | 'top-bar'; +type ExtensionsType = ResourceTabExtension | SystemLevelExtension | AppViewExtension | StatusPanelExtension | TopBarActionMenuExt; class ExtensionsEventTarget { private listeners: Map void>> = new Map(); @@ -34,7 +34,8 @@ const extensions = { resourceExtentions: new Array(), systemLevelExtensions: new Array(), appViewExtensions: new Array(), - statusPanelExtensions: new Array() + statusPanelExtensions: new Array(), + topBarActionMenuExts: new Array() }; function registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string, opts?: {icon: string}) { @@ -61,6 +62,20 @@ function registerStatusPanelExtension(component: StatusPanelExtensionComponent, extensions.eventTarget.emit('statusPanel', ext); } +function registerTopBarActionMenuExt( + component: TopBarActionMenuExtComponent, + title: string, + id: string, + flyout: ExtensionComponent, + shouldDisplay: (app?: Application) => boolean = () => true, + iconClassName?: string, + isMiddle = false +) { + const ext = {component, flyout, shouldDisplay, title, id, iconClassName, isMiddle}; + extensions.topBarActionMenuExts.push(ext); + extensions.eventTarget.emit('top-bar', ext); +} + let legacyInitialized = false; function initLegacyExtensions() { @@ -103,11 +118,24 @@ export interface StatusPanelExtension { id: string; } +export interface TopBarActionMenuExt { + component: TopBarActionMenuExtComponent; + flyout: TopBarActionMenuExtFlyoutComponent; + shouldDisplay: (app: Application) => boolean; + title: string; + id: string; + iconClassName?: string; + isMiddle?: boolean; + isNarrow?: boolean; +} + export type ExtensionComponent = React.ComponentType; export type SystemExtensionComponent = React.ComponentType; export type AppViewExtensionComponent = React.ComponentType; export type StatusPanelExtensionComponent = React.ComponentType; export type StatusPanelExtensionFlyoutComponent = React.ComponentType; +export type TopBarActionMenuExtComponent = React.ComponentType; +export type TopBarActionMenuExtFlyoutComponent = React.ComponentType; export interface Extension { component: ExtensionComponent; @@ -129,11 +157,22 @@ export interface StatusPanelComponentProps { openFlyout: () => any; } +export interface TopBarActionMenuExtComponentProps { + application: Application; + tree: ApplicationTree; + openFlyout: () => any; +} + export interface StatusPanelFlyoutProps { application: Application; tree: ApplicationTree; } +export interface TopBarActionMenuExtFlyoutProps { + application: Application; + tree: ApplicationTree; +} + export class ExtensionsService { public addEventListener(evtType: ExtensionsEventType, cb: (ext: ExtensionsType) => void) { extensions.eventTarget.addEventListener(evtType, cb); @@ -160,6 +199,9 @@ export class ExtensionsService { public getStatusPanelExtensions(): StatusPanelExtension[] { return extensions.statusPanelExtensions.slice(); } + public getActionMenuExtensions(): TopBarActionMenuExt[] { + return extensions.topBarActionMenuExts.slice(); + } } ((window: any) => { @@ -169,6 +211,7 @@ export class ExtensionsService { registerResourceExtension, registerSystemLevelExtension, registerAppViewExtension, - registerStatusPanelExtension + registerStatusPanelExtension, + registerTopBarActionMenuExt }; })(window);