Skip to content

Commit

Permalink
feat(ui): add extension the top-bar action menu (argoproj#19620)
Browse files Browse the repository at this point in the history
* add topbar action menu  ext
Signed-off-by: AS <11219262+ashutosh16@users.noreply.github.com>
Co-authored-by: ashutosh16 <ashutosh_singh@intuit.com>
Co-authored-by: Anton Gilgur <4970083+agilgur5@users.noreply.github.com>
  • Loading branch information
ashutosh16 authored Aug 29, 2024
1 parent 2c8a574 commit 00466c3
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 31 deletions.
101 changes: 82 additions & 19 deletions docs/developer-guide/extensions/ui-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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);
```
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,6 +44,8 @@ interface ApplicationDetailsState {
extensionsMap?: {[key: string]: AppViewExtension};
statusExtensions?: StatusPanelExtension[];
statusExtensionsMap?: {[key: string]: StatusPanelExtension};
topBarActionMenuExts?: TopBarActionMenuExt[];
topBarActionMenuExtsMap?: {[key: string]: TopBarActionMenuExt};
}

interface FilterInput {
Expand Down Expand Up @@ -94,6 +96,11 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
statusExtensions.forEach(ext => {
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: [],
Expand All @@ -104,7 +111,9 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
extensions,
extensionsMap,
statusExtensions,
statusExtensionsMap
statusExtensionsMap,
topBarActionMenuExts,
topBarActionMenuExtsMap
};
if (typeof this.props.match.params.appnamespace === 'undefined') {
this.appNamespace = '';
Expand Down Expand Up @@ -567,7 +576,8 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
namespace: application.metadata.namespace
});

const activeExtension = this.state.statusExtensionsMap[this.selectedExtension];
const activeStatusExt = this.state.statusExtensionsMap[this.selectedExtension];
const activeTopBarActionMenuExt = this.state.topBarActionMenuExtsMap[this.selectedExtension];

return (
<div className={`application-details ${this.props.match.params.name}`}>
Expand All @@ -580,7 +590,14 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
{title: 'Applications', path: '/applications'},
{title: <ApplicationsDetailsAppDropdown appName={this.props.match.params.name} />}
],
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: (
<React.Fragment key='app-list-tools'>
<div className='application-details__view-type'>
Expand Down Expand Up @@ -866,10 +883,16 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
)}
</SlidingPanel>
<SlidingPanel
isShown={this.selectedExtension !== '' && activeExtension != null && activeExtension.flyout != null}
isShown={this.selectedExtension !== '' && activeStatusExt != null && activeStatusExt.flyout != null}
onClose={() => this.setExtensionPanelVisible('')}>
{this.selectedExtension !== '' && activeStatusExt?.flyout && <activeStatusExt.flyout application={application} tree={tree} />}
</SlidingPanel>
<SlidingPanel
isMiddle={activeTopBarActionMenuExt?.isMiddle}
isShown={this.selectedExtension !== '' && activeTopBarActionMenuExt != null && activeTopBarActionMenuExt.flyout != null}
onClose={() => this.setExtensionPanelVisible('')}>
{this.selectedExtension !== '' && activeExtension && activeExtension.flyout && (
<activeExtension.flyout application={application} tree={tree} />
{this.selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && (
<activeTopBarActionMenuExt.flyout application={application} tree={tree} />
)}
</SlidingPanel>
</Page>
Expand All @@ -881,7 +904,13 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
</ObservableQuery>
);
}

private renderActionMenuItem(ext: TopBarActionMenuExt, tree: appModels.ApplicationTree, application: appModels.Application, showExtension?: (id: string) => any): any {
return {
action: () => this.setExtensionPanelVisible(ext.id),
title: <ext.component application={application} tree={tree} openFlyout={() => 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});
Expand Down
51 changes: 47 additions & 4 deletions ui/src/app/shared/services/extensions-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExtensionsEventType, Array<(extension: ExtensionsType) => void>> = new Map();
Expand Down Expand Up @@ -34,7 +34,8 @@ const extensions = {
resourceExtentions: new Array<ResourceTabExtension>(),
systemLevelExtensions: new Array<SystemLevelExtension>(),
appViewExtensions: new Array<AppViewExtension>(),
statusPanelExtensions: new Array<StatusPanelExtension>()
statusPanelExtensions: new Array<StatusPanelExtension>(),
topBarActionMenuExts: new Array<TopBarActionMenuExt>()
};

function registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string, opts?: {icon: string}) {
Expand All @@ -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() {
Expand Down Expand Up @@ -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<ExtensionComponentProps>;
export type SystemExtensionComponent = React.ComponentType;
export type AppViewExtensionComponent = React.ComponentType<AppViewComponentProps>;
export type StatusPanelExtensionComponent = React.ComponentType<StatusPanelComponentProps>;
export type StatusPanelExtensionFlyoutComponent = React.ComponentType<StatusPanelFlyoutProps>;
export type TopBarActionMenuExtComponent = React.ComponentType<TopBarActionMenuExtComponentProps>;
export type TopBarActionMenuExtFlyoutComponent = React.ComponentType<TopBarActionMenuExtFlyoutProps>;

export interface Extension {
component: ExtensionComponent;
Expand All @@ -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);
Expand All @@ -160,6 +199,9 @@ export class ExtensionsService {
public getStatusPanelExtensions(): StatusPanelExtension[] {
return extensions.statusPanelExtensions.slice();
}
public getActionMenuExtensions(): TopBarActionMenuExt[] {
return extensions.topBarActionMenuExts.slice();
}
}

((window: any) => {
Expand All @@ -169,6 +211,7 @@ export class ExtensionsService {
registerResourceExtension,
registerSystemLevelExtension,
registerAppViewExtension,
registerStatusPanelExtension
registerStatusPanelExtension,
registerTopBarActionMenuExt
};
})(window);

0 comments on commit 00466c3

Please sign in to comment.