Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add status panel extensions #15780

Merged
merged 16 commits into from
Dec 18, 2023
63 changes: 63 additions & 0 deletions docs/developer-guide/extensions/ui-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,66 @@ Below is an example of a simple system level extension:

Since the Argo CD Application is a Kubernetes resource, application tabs can be the same as any other resource tab.
Make sure to use 'argoproj.io'/'Application' as group/kind and an extension will be used to render the application-level tab.

## Application Status Panel Extensions

The status panel is the bar at the top of the application view where the sync status is displayed. Argo CD allows you to add new items to the status panel of an application. The extension should be registered using the `extensionsAPI.registerStatusPanelExtension` method:

```typescript
registerStatusPanelExtension(component: StatusPanelExtensionComponent, title: string, id: string, flyout?: ExtensionComponent)
```

Below is an example of a simple extension:

```typescript
((window) => {
const component = () => {
return React.createElement(
"div",
{ style: { padding: "10px" } },
"Hello World"
);
};
window.extensionsAPI.registerStatusPanelExtension(
component,
"My Extension",
"my_extension"
);
})(window);
```

### Flyout widget

It is also possible to add an optional flyout widget to your extension. It can be opened by calling `openFlyout()` from your extension's component. Your flyout component will then be rendered in a sliding panel, similar to the panel that opens when clicking on `History and rollback`.

Below is an example of an extension using the flyout widget:

```typescript
((window) => {
const component = (props: {
openFlyout: () => any
}) => {
return React.createElement(
"div",
{
style: { padding: "10px" },
onClick: () => props.openFlyout()
},
"Hello World"
);
};
const flyout = () => {
return React.createElement(
"div",
{ style: { padding: "10px" } },
"This is a flyout"
);
};
window.extensionsAPI.registerStatusPanelExtension(
component,
"My Extension",
"my_extension",
flyout
);
})(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} from '../../../shared/services/extensions-service';
import {AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service';

interface ApplicationDetailsState {
page: number;
Expand All @@ -42,6 +42,8 @@ interface ApplicationDetailsState {
collapsedNodes?: string[];
extensions?: AppViewExtension[];
extensionsMap?: {[key: string]: AppViewExtension};
statusExtensions?: StatusPanelExtension[];
statusExtensionsMap?: {[key: string]: StatusPanelExtension};
}

interface FilterInput {
Expand Down Expand Up @@ -87,6 +89,11 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
extensions.forEach(ext => {
extensionsMap[ext.title] = ext;
});
const statusExtensions = services.extensions.getStatusPanelExtensions();
const statusExtensionsMap: {[key: string]: StatusPanelExtension} = {};
statusExtensions.forEach(ext => {
statusExtensionsMap[ext.id] = ext;
});
this.state = {
page: 0,
groupedResources: [],
Expand All @@ -95,7 +102,9 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
truncateNameOnRight: false,
collapsedNodes: [],
extensions,
extensionsMap
extensionsMap,
statusExtensions,
statusExtensionsMap
};
if (typeof this.props.match.params.appnamespace === 'undefined') {
this.appNamespace = '';
Expand Down Expand Up @@ -142,6 +151,10 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
return nodeContainer.key;
}

private get selectedExtension() {
return new URLSearchParams(this.props.history.location.search).get('extension');
}

private closeGroupedNodesPanel() {
this.setState({groupedResources: []});
this.setState({slidingPanelPage: 0});
Expand Down Expand Up @@ -353,6 +366,9 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
name: application.metadata.name,
namespace: application.metadata.namespace
});

const activeExtension = this.state.statusExtensionsMap[this.selectedExtension];

return (
<div className={`application-details ${this.props.match.params.name}`}>
<Page
Expand Down Expand Up @@ -423,6 +439,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
showDiff={() => this.selectNode(appFullName, 0, 'diff')}
showOperation={() => this.setOperationStatusVisible(true)}
showConditions={() => this.setConditionsStatusVisible(true)}
showExtension={id => this.setExtensionPanelVisible(id)}
showMetadataInfo={revision => this.setState({...this.state, revision})}
/>
</div>
Expand Down Expand Up @@ -732,6 +749,13 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
</DataLoader>
))}
</SlidingPanel>
<SlidingPanel
isShown={this.selectedExtension !== '' && activeExtension != null && activeExtension.flyout != null}
onClose={() => this.setExtensionPanelVisible('')}>
{this.selectedExtension !== '' && activeExtension && activeExtension.flyout && (
<activeExtension.flyout application={application} tree={tree} />
)}
</SlidingPanel>
</Page>
</div>
);
Expand Down Expand Up @@ -966,6 +990,10 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
this.appContext.apis.navigation.goto('.', {rollback: selectedDeploymentIndex}, {replace: true});
}

private setExtensionPanelVisible(selectedExtension = '') {
this.appContext.apis.navigation.goto('.', {extension: selectedExtension}, {replace: true});
}

private selectNode(fullName: string, containerIndex = 0, tab: string = null) {
SelectNode(fullName, containerIndex, tab, this.appContext.apis);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Props {
showDiff?: () => any;
showOperation?: () => any;
showConditions?: () => any;
showExtension?: (id: string) => any;
showMetadataInfo?: (revision: string) => any;
}

Expand Down Expand Up @@ -45,7 +46,7 @@ const sectionHeader = (info: SectionInfo, hasMultipleSources: boolean, onClick?:
);
};

export const ApplicationStatusPanel = ({application, showDiff, showOperation, showConditions, showMetadataInfo}: Props) => {
export const ApplicationStatusPanel = ({application, showDiff, showOperation, showConditions, showExtension, showMetadataInfo}: Props) => {
const today = new Date();

let daysSinceLastSynchronized = 0;
Expand All @@ -63,6 +64,8 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
showOperation = null;
}

const statusExtensions = services.extensions.getStatusPanelExtensions();

const infos = cntByCategory.get('info');
const warnings = cntByCategory.get('warning');
const errors = cntByCategory.get('error');
Expand Down Expand Up @@ -203,6 +206,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
</React.Fragment>
)}
</DataLoader>
{statusExtensions && statusExtensions.map(ext => <ext.component key={ext.title} application={application} openFlyout={() => showExtension && showExtension(ext.id)} />)}
</div>
);
};
33 changes: 31 additions & 2 deletions ui/src/app/shared/services/extensions-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {Application, ApplicationTree, State} from '../models';
const extensions = {
resourceExtentions: new Array<ResourceTabExtension>(),
systemLevelExtensions: new Array<SystemLevelExtension>(),
appViewExtensions: new Array<AppViewExtension>()
appViewExtensions: new Array<AppViewExtension>(),
statusPanelExtensions: new Array<StatusPanelExtension>()
};

function registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string, opts?: {icon: string}) {
Expand All @@ -21,6 +22,10 @@ function registerAppViewExtension(component: ExtensionComponent, title: string,
extensions.appViewExtensions.push({component, title, icon});
}

function registerStatusPanelExtension(component: StatusPanelExtensionComponent, title: string, id: string, flyout?: ExtensionComponent) {
extensions.statusPanelExtensions.push({component, flyout, title, id});
}

let legacyInitialized = false;

function initLegacyExtensions() {
Expand Down Expand Up @@ -56,9 +61,18 @@ export interface AppViewExtension {
icon?: string;
}

export interface StatusPanelExtension {
component: StatusPanelExtensionComponent;
flyout?: StatusPanelExtensionFlyoutComponent;
title: string;
id: string;
}

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 interface Extension {
component: ExtensionComponent;
Expand All @@ -75,6 +89,16 @@ export interface AppViewComponentProps {
tree: ApplicationTree;
}

export interface StatusPanelComponentProps {
application: Application;
openFlyout: () => any;
}

export interface StatusPanelFlyoutProps {
application: Application;
tree: ApplicationTree;
}

export class ExtensionsService {
public getResourceTabs(group: string, kind: string): ResourceTabExtension[] {
initLegacyExtensions();
Expand All @@ -89,6 +113,10 @@ export class ExtensionsService {
public getAppViewExtensions(): AppViewExtension[] {
return extensions.appViewExtensions.slice();
}

public getStatusPanelExtensions(): StatusPanelExtension[] {
return extensions.statusPanelExtensions.slice();
}
}

((window: any) => {
Expand All @@ -97,6 +125,7 @@ export class ExtensionsService {
window.extensionsAPI = {
registerResourceExtension,
registerSystemLevelExtension,
registerAppViewExtension
registerAppViewExtension,
registerStatusPanelExtension
};
})(window);
Loading