Skip to content

Commit 64d05ea

Browse files
Alexander MatyushentsevAlexander Matyushentsev
Alexander Matyushentsev
authored and
Alexander Matyushentsev
committed
Move notification manager and popup manager from argo-cd-ui
1 parent 55c5932 commit 64d05ea

File tree

11 files changed

+300
-38
lines changed

11 files changed

+300
-38
lines changed

src/app/app.tsx

+54-26
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as React from 'react';
44
import { Redirect, Route, RouteComponentProps, Router, Switch } from 'react-router';
55

66
import { uiUrl } from './shared/base';
7-
import { Layout } from './shared/components';
7+
import { Layout, NotificationInfo, Notifications, NotificationsManager, Popup, PopupManager, PopupProps } from './shared/components';
88
import { AppContext } from './shared/context';
99

1010
export const history = createHistory();
@@ -30,28 +30,56 @@ const navItems = [{
3030
iconClassName: 'argo-icon-docs',
3131
}];
3232

33-
export const App = () => (
34-
<div>
35-
<Router history={history}>
36-
<Switch>
37-
<Redirect exact={true} path={uiUrl('')} to={workflowsUrl}/>
38-
<Route path={timelineUrl} component={ class ToWorkflows extends React.Component {
39-
public static contextTypes = { router: PropTypes.object };
40-
public render() {return <div/>; }
41-
42-
public componentWillMount() {
43-
const router = (this.context as AppContext).router;
44-
router.history.push(router.route.location.pathname.replace(timelineUrl, workflowsUrl));
45-
}
46-
} }/>
47-
<Layout navItems={navItems}>
48-
{/* <NotificationsContainer /> */}
49-
{Object.keys(routes).map((path) => {
50-
const route = routes[path];
51-
return <Route key={path} path={path} component={route.component}/>;
52-
})}
53-
</Layout>
54-
</Switch>
55-
</Router>
56-
</div>
57-
);
33+
export class App extends React.Component<{}, { notifications: NotificationInfo[], popupProps: PopupProps }> {
34+
public static childContextTypes = {
35+
history: PropTypes.object,
36+
apis: PropTypes.object,
37+
};
38+
39+
private popupManager: PopupManager;
40+
private notificationsManager: NotificationsManager;
41+
42+
constructor(props: {}) {
43+
super(props);
44+
this.state = { notifications: [], popupProps: null };
45+
this.popupManager = new PopupManager();
46+
this.notificationsManager = new NotificationsManager();
47+
}
48+
49+
public componentDidMount() {
50+
this.popupManager.popupProps.subscribe((popupProps) => this.setState({ popupProps }));
51+
this.notificationsManager.notifications.subscribe((notifications) => this.setState({ notifications }));
52+
}
53+
54+
public render() {
55+
return (
56+
<div>
57+
{this.state.popupProps && <Popup {...this.state.popupProps}/>}
58+
<Router history={history}>
59+
<Switch>
60+
<Redirect exact={true} path={uiUrl('')} to={workflowsUrl}/>
61+
<Route path={timelineUrl} component={ class ToWorkflows extends React.Component {
62+
public static contextTypes = { router: PropTypes.object };
63+
public render() {return <div/>; }
64+
public componentWillMount() {
65+
const router = (this.context as AppContext).router;
66+
router.history.push(router.route.location.pathname.replace(timelineUrl, workflowsUrl));
67+
}
68+
} }/>
69+
<Layout navItems={navItems}>
70+
<Notifications leftOffset={60} closeNotification={(item) => this.notificationsManager.close(item)} notifications={this.state.notifications}/>
71+
{Object.keys(routes).map((path) => {
72+
const route = routes[path];
73+
return <Route key={path} path={path} component={route.component}/>;
74+
})}
75+
</Layout>
76+
</Switch>
77+
</Router>
78+
</div>
79+
);
80+
}
81+
82+
public getChildContext() {
83+
return { history, apis: { popup: this.popupManager, notifications: this.notificationsManager } };
84+
}
85+
}

src/app/shared/components/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ export { Duration } from './duration';
1313
export { SlidingPanel } from './sliding-panel/sliding-panel';
1414
export { LogsViewer } from './logs-viewer/logs-viewer';
1515
export * from './notifications/notifications';
16+
export * from './notifications/notification-manager';
17+
export * from './popup/popup';
18+
export * from './popup/popup-manager';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BehaviorSubject, Observable } from 'rxjs';
2+
import { NotificationInfo } from './notifications';
3+
4+
export interface NotificationsApi {
5+
show(notification: NotificationInfo, autoHideMs?: number): void;
6+
close(notification: NotificationInfo): void;
7+
}
8+
9+
export class NotificationsManager {
10+
private notificationsSubject: BehaviorSubject<NotificationInfo[]> = new BehaviorSubject([]);
11+
12+
public get notifications(): Observable<NotificationInfo[]> {
13+
return this.notificationsSubject.asObservable();
14+
}
15+
16+
public show(notification: NotificationInfo, autoHideMs = 5000) {
17+
this.notificationsSubject.next([...(this.notificationsSubject.getValue() || []), notification]);
18+
if (autoHideMs > -1) {
19+
setTimeout(() => this.close(notification), autoHideMs);
20+
}
21+
}
22+
23+
public close(notification: NotificationInfo): void {
24+
const notifications = (this.notificationsSubject.getValue() || []).slice();
25+
const index = this.notificationsSubject.getValue().indexOf(notification);
26+
if (index > -1) {
27+
notifications.splice(index, 1);
28+
this.notificationsSubject.next(notifications);
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from 'react';
2+
import { BehaviorSubject, Observable } from 'rxjs';
3+
import { PopupProps } from './popup';
4+
5+
export interface PopupApi {
6+
confirm(title: string, message: string | React.ReactNode): Promise<boolean>;
7+
}
8+
9+
export class PopupManager implements PopupApi {
10+
private popupPropsSubject: BehaviorSubject<PopupProps> = new BehaviorSubject(null);
11+
12+
public get popupProps(): Observable<PopupProps> {
13+
return this.popupPropsSubject.asObservable();
14+
}
15+
16+
public confirm(title: string, message: string): Promise<boolean> {
17+
return new Promise((resolve) => {
18+
const closeAndResolve = (result: boolean) => {
19+
this.popupPropsSubject.next(null);
20+
resolve(result);
21+
};
22+
23+
this.popupPropsSubject.next({
24+
title: (
25+
<span>{title} <i className='argo-icon-close' onClick={() => closeAndResolve(false)}/></span>
26+
),
27+
content: (
28+
<div>
29+
<p>{message}</p>
30+
</div>
31+
),
32+
footer: (
33+
<div>
34+
<button className='argo-button argo-button--base' onClick={() => closeAndResolve(true)}>Yes</button> <button
35+
className='argo-button argo-button--base-o' onClick={() => closeAndResolve(false)}>Cancel</button>
36+
</div>
37+
),
38+
});
39+
});
40+
}
41+
}
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
@import '../../styles/config';
2+
3+
$popup-border-radius: 8px;
4+
5+
.popup-overlay {
6+
position: fixed;
7+
top: 0;
8+
left: 0;
9+
width: 100%;
10+
height: 100%;
11+
z-index: 200;
12+
background-color: rgba(222, 222, 222, 0.62); /*dim the background*/
13+
border: 1px solid $argo-color-gray-4;
14+
15+
.popup-container {
16+
position: relative;
17+
box-shadow: 3px 3px 20px #888888;
18+
margin: 0 auto;
19+
top: 25%;
20+
width: 35%;
21+
min-width: 400px;
22+
max-width: 1000px;
23+
max-height: 600px;
24+
z-index: 20;
25+
border-radius: $popup-border-radius;
26+
27+
&__header {
28+
line-height: 60px;
29+
padding-left: 30px;
30+
height: 60px;
31+
background-color: $argo-color-gray-2;
32+
border-top-right-radius: $popup-border-radius;
33+
border-top-left-radius: $popup-border-radius;
34+
35+
h2 {
36+
margin-left: 5%;
37+
line-height: 50px;
38+
}
39+
40+
i {
41+
font-size: 21px;
42+
float: right;
43+
position: absolute;
44+
right: 20px;
45+
top: 20px;
46+
cursor: pointer;
47+
}
48+
}
49+
50+
&__footer {
51+
background-color: white;
52+
bottom: 0;
53+
padding-top: 10px;
54+
padding-left: 44px;
55+
padding-bottom: 40px;
56+
border-bottom-right-radius: $popup-border-radius;
57+
border-bottom-left-radius: $popup-border-radius;
58+
59+
& > .ax-button {
60+
min-width: 120px;
61+
margin-right: 20px;
62+
}
63+
64+
&--additional-padding {
65+
padding-left: 154px;
66+
}
67+
}
68+
69+
&__body {
70+
background-color: white;
71+
72+
h4, p {
73+
padding-left: 30px;
74+
margin-top: 40px;
75+
margin-right: 10px;
76+
word-wrap: break-word;
77+
}
78+
79+
h4 {
80+
font-size: 15px;
81+
font-weight: bold;
82+
}
83+
84+
p {
85+
86+
font-size: 15px;
87+
margin-top: 10px;
88+
margin-bottom: 10px;
89+
color: $argo-color-gray-6;
90+
}
91+
}
92+
93+
&__icon {
94+
transform: translateY(36%);
95+
96+
& > i {
97+
display: block;
98+
text-align: end;
99+
font-size: 40px;
100+
101+
&.warning {
102+
color: $argo-color-yellow;
103+
}
104+
105+
&.success {
106+
color: $argo-success-color;
107+
}
108+
109+
&.failed {
110+
color: $argo-failed-color;
111+
}
112+
}
113+
}
114+
}
115+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as classNames from 'classnames';
2+
import * as React from 'react';
3+
4+
export interface PopupProps extends React.Props<any> {
5+
icon?: { name: string; color: string; };
6+
title: string | React.ReactNode;
7+
content?: React.ReactNode;
8+
footer?: React.ReactNode;
9+
}
10+
11+
require('./popup.scss');
12+
13+
export const Popup = (props: PopupProps) => (
14+
<div className='popup-overlay'>
15+
<div className='popup-container'>
16+
<div className='row popup-container__header'>
17+
{props.title}
18+
</div>
19+
<div className='row popup-container__body'>
20+
{props.icon &&
21+
<div className='columns large-2 popup-container__icon'>
22+
<i className={`${props.icon.name} ${props.icon.color}`}/>
23+
</div>
24+
}
25+
<div className={classNames('columns', {'large-10': !!props.icon, 'large-12': !props.icon})}>
26+
{props.content}
27+
</div>
28+
</div>
29+
30+
<div className={classNames('row popup-container__footer', {'popup-container__footer--additional-padding': !!props.icon})}>
31+
{props.footer}
32+
</div>
33+
</div>
34+
</div>
35+
);

src/app/shared/context.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as H from 'history';
22
import { match} from 'react-router';
3+
import { NotificationsApi, PopupApi } from './components';
34

45
export interface AppContext {
56
router: {
@@ -9,4 +10,9 @@ export interface AppContext {
910
match: match<any>;
1011
};
1112
};
13+
apis: {
14+
popup: PopupApi;
15+
notifications: NotificationsApi;
16+
};
17+
history: H.History;
1218
}

src/app/workflows/components/workflow-details/workflow-details.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Observable, Subscription } from 'rxjs';
66

77
import * as models from '../../../../models';
88
import { uiUrl } from '../../../shared/base';
9-
import { LogsViewer, Page, SlidingPanel } from '../../../shared/components';
9+
import { LogsViewer, NotificationType, Page, SlidingPanel } from '../../../shared/components';
1010
import { AppContext } from '../../../shared/context';
1111
import { services } from '../../../shared/services';
1212

@@ -31,6 +31,7 @@ export class WorkflowDetails extends React.Component<RouteComponentProps<any>, {
3131

3232
public static contextTypes = {
3333
router: PropTypes.object,
34+
apis: PropTypes.object,
3435
};
3536

3637
private changesSubscription: Subscription;
@@ -208,10 +209,10 @@ export class WorkflowDetails extends React.Component<RouteComponentProps<any>, {
208209
this.setState({ workflow });
209210
});
210211
} catch (e) {
211-
// dispatch(commonActions.showNotification({
212-
// content: 'Unable to load workflow',
213-
// type: NotificationType.Error,
214-
// }));
212+
this.appContext.apis.notifications.show({
213+
content: 'Unable to load workflow',
214+
type: NotificationType.Error,
215+
});
215216
}
216217
}
217218

0 commit comments

Comments
 (0)