diff --git a/client/src/components/main/notification/NotificationBrowser.css b/client/src/components/main/notification/NotificationBrowser.css
index ca6e5ae53d..d4046fefd6 100644
--- a/client/src/components/main/notification/NotificationBrowser.css
+++ b/client/src/components/main/notification/NotificationBrowser.css
@@ -50,7 +50,7 @@
.notification-grid-row {
display: grid;
- grid: "status title body date readDate" 34px / 55px 1fr 1fr 135px 135px;
+ grid: "status title body date readDate actions" 34px / 55px 1fr 1fr 135px 135px 25px;
padding: 3px 0;
cursor: pointer;
transition: all 0.3s ease;
@@ -97,6 +97,10 @@
grid-area: readDate;
}
+.notification-actions {
+ grid-area: actions;
+}
+
.empty-placeholder {
padding: 15px;
text-align: center;
diff --git a/client/src/components/main/notification/NotificationBrowser.js b/client/src/components/main/notification/NotificationBrowser.js
index 3bb7989f2a..25eeb27b9c 100644
--- a/client/src/components/main/notification/NotificationBrowser.js
+++ b/client/src/components/main/notification/NotificationBrowser.js
@@ -33,6 +33,7 @@ import NotificationsRequest from '../../../models/notifications/CurrentUserNotif
import ReadAllUserNotifications from '../../../models/notifications/ReadAllUserNotifications';
import displayDate from '../../../utils/displayDate';
import PreviewNotification from './PreviewNotification';
+import NotificationActions from './notification-actions';
import styles from './NotificationBrowser.css';
const PAGE_SIZE = 20;
@@ -54,7 +55,7 @@ function dateSorter (a, b) {
}
};
-@inject('userNotifications')
+@inject('userNotifications', 'router')
@observer
export default class NotificationBrowser extends React.Component {
state = {
@@ -214,6 +215,11 @@ export default class NotificationBrowser extends React.Component {
)}>
Read date
+
);
};
@@ -288,6 +294,18 @@ export default class NotificationBrowser extends React.Component {
)}>
{displayDate(notification.readDate, 'YYYY-MM-DD HH:mm:ss')}
+ e.stopPropagation()}
+ >
+
+
))
: emptyPlaceholder
diff --git a/client/src/components/main/notification/NotificationCenter.js b/client/src/components/main/notification/NotificationCenter.js
index bb217e37b5..0f81f03066 100644
--- a/client/src/components/main/notification/NotificationCenter.js
+++ b/client/src/components/main/notification/NotificationCenter.js
@@ -61,7 +61,9 @@ function mapMessage (message) {
isRead: message.isRead,
userId: message.userId,
notificationId: `message_${message.id}`,
- type: NOTIFICATION_TYPE.message
+ notificationType: NOTIFICATION_TYPE.message,
+ resources: message.resources,
+ type: message.type
};
}
@@ -309,7 +311,7 @@ export default class NotificationCenter extends React.Component {
id: notification.notificationId,
createdDate: notification.createdDate
});
- if (notification.type === NOTIFICATION_TYPE.message) {
+ if (notification.notificationType === NOTIFICATION_TYPE.message) {
this.readMessage(notification);
}
if (notification.blocking) {
@@ -459,7 +461,7 @@ export default class NotificationCenter extends React.Component {
openPreviewNotification = (notification) => {
this.setState({previewNotification: notification}, () => {
- if (notification.type === NOTIFICATION_TYPE.message) {
+ if (notification.notificationType === NOTIFICATION_TYPE.message) {
this.readMessage(notification, true);
}
});
@@ -489,8 +491,8 @@ export default class NotificationCenter extends React.Component {
onHeightInitialized={this.onHeightInitialized}
key={notification.notificationId || notification.createdDate}
notification={notification}
- type={notification.type}
- onClick={notification.type === NOTIFICATION_TYPE.message
+ type={notification.notificationType}
+ onClick={notification.notificationType === NOTIFICATION_TYPE.message
? this.openPreviewNotification
: undefined
}
diff --git a/client/src/components/main/notification/SystemNotification.js b/client/src/components/main/notification/SystemNotification.js
index 160d694915..fd50e2348b 100644
--- a/client/src/components/main/notification/SystemNotification.js
+++ b/client/src/components/main/notification/SystemNotification.js
@@ -22,6 +22,7 @@ import classNames from 'classnames';
import displayDate from '../../../utils/displayDate';
import PreviewNotification from './PreviewNotification';
import {NOTIFICATION_TYPE} from './NotificationCenter';
+import NotificationActions from './notification-actions';
import styles from './SystemNotification.css';
@observer
@@ -200,6 +201,11 @@ export default class SystemNotification extends React.Component {
>
{displayDate(this.props.notification.createdDate)}
+
diff --git a/client/src/components/main/notification/notification-actions/actions.js b/client/src/components/main/notification/notification-actions/actions.js
new file mode 100644
index 0000000000..f4e1f78e47
--- /dev/null
+++ b/client/src/components/main/notification/notification-actions/actions.js
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2017-2023 EPAM Systems, Inc. (https://www.epam.com/)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {message} from 'antd';
+import moment from 'moment-timezone';
+import StopPipeline from '../../../../models/pipelines/StopPipeline';
+import ResumePipeline from '../../../../models/pipelines/ResumePipeline';
+import PausePipeline from '../../../../models/pipelines/PausePipeline';
+import TerminatePipeline from '../../../../models/pipelines/TerminatePipeline';
+import DataStorageLifeCycleRulesPostpone
+from '../../../../models/dataStorage/lifeCycleRules/DataStorageLifeCycleRulesPostpone';
+import {canPauseRun, canStopRun} from '../../../runs/actions';
+import RunStatuses from '../../../special/run-status-icon/run-statuses';
+
+const ACTIONS = {
+ viewRun: {
+ key: 'View run',
+ actionFn: ({entity, router}) => {
+ router && router.push(`/run/${entity.id}`);
+ },
+ available: () => true
+ },
+ pauseRun: {
+ key: 'Pause run',
+ actionFn: async ({entity, callback}) => {
+ const hide = message.loading('Pausing...', -1);
+ const request = new PausePipeline(entity.id);
+ await request.send({});
+ if (request.error) {
+ message.error(request.error);
+ }
+ hide();
+ callback && callback();
+ },
+ available: (entity, preferences) => entity && preferences && canPauseRun(entity, preferences)
+ },
+ resumeRun: {
+ key: 'Resume run',
+ actionFn: async ({entity, callback}) => {
+ const hide = message.loading('Resuming...', -1);
+ const request = new ResumePipeline(entity.id);
+ await request.send({});
+ if (request.error) {
+ message.error(request.error);
+ }
+ hide();
+ callback && callback();
+ },
+ available: (entity) => entity && entity.status === RunStatuses.paused
+ },
+ stopRun: {
+ key: 'Stop run',
+ actionFn: async ({entity, callback}) => {
+ const hide = message.loading('Stopping...', -1);
+ const request = new StopPipeline(entity.id);
+ await request.send({
+ endDate: moment().format('YYYY-MM-DD HH:mm:ss.SSS'),
+ status: 'STOPPED'
+ });
+ if (request.error) {
+ message.error(request.error);
+ }
+ hide();
+ callback && callback();
+ },
+ available: (entity) => entity && canStopRun(entity)
+ },
+ terminateRun: {
+ key: 'Terminate run',
+ actionFn: async ({entity, callback}) => {
+ const hide = message.loading('Terminating run...', -1);
+ const request = new TerminatePipeline(entity.id);
+ await request.send({});
+ if (request.error) {
+ message.error(request.error);
+ }
+ hide();
+ callback && callback();
+ },
+ available: (entity) => entity && entity.status === RunStatuses.paused
+ },
+ openDatastorage: {
+ key: 'Open datastorage',
+ actionFn: ({notification = {}, router}) => {
+ const details = (notification.resources || [])[0] || {};
+ const {entityId} = details;
+ router && entityId && router.push(`/storage/${entityId}`);
+ },
+ available: () => true
+ },
+ postponeLifecycleRule: {
+ key: 'Postpone',
+ actionFn: async ({notification = {}, callback}) => {
+ const details = (notification.resources || [])[0] || {};
+ const hide = message.loading('Postpone...', -1);
+ const request = new DataStorageLifeCycleRulesPostpone({
+ datastorageId: details.entityId,
+ ruleId: details.storageRuleId,
+ path: details.storagePath
+ });
+ await request.fetch();
+ if (request.error) {
+ message.error(request.error);
+ }
+ hide();
+ callback && callback();
+ },
+ available: () => true
+ },
+ viewBilling: {
+ key: 'View billing',
+ actionFn: ({router}) => {
+ router && router.push('/billing/reports/storage');
+ },
+ available: () => true
+ },
+ openPoolsUsage: {
+ key: 'Open pools usage statistics',
+ actionFn: ({router}) => {
+ router && router.push('/cluster/usage');
+ },
+ available: () => true
+ }
+};
+
+const ENTITY_CLASSES = {
+ RUN: 'RUN',
+ STORAGE: 'STORAGE',
+ ISSUE: 'ISSUE',
+ QUOTA: 'QUOTA',
+ NODE_POOL: 'NODE_POOL',
+ USER: 'USER'
+};
+
+const NOTIFICATION_TYPES = {
+ BILLING_QUOTA_EXCEEDING: 'BILLING_QUOTA_EXCEEDING',
+ DATASTORAGE_LIFECYCLE_ACTION: 'DATASTORAGE_LIFECYCLE_ACTION',
+ DATASTORAGE_LIFECYCLE_RESTORE_ACTION: 'DATASTORAGE_LIFECYCLE_RESTORE_ACTION',
+ FULL_NODE_POOL: 'FULL_NODE_POOL',
+ HIGH_CONSUMED_RESOURCES: 'HIGH_CONSUMED_RESOURCES',
+ IDLE_RUN: 'IDLE_RUN',
+ IDLE_RUN_PAUSED: 'IDLE_RUN_PAUSED',
+ IDLE_RUN_STOPPED: 'IDLE_RUN_STOPPED',
+ LONG_INIT: 'LONG_INIT',
+ LONG_PAUSED: 'LONG_PAUSED',
+ LONG_PAUSED_STOPPED: 'LONG_PAUSED_STOPPED',
+ LONG_RUNNING: 'LONG_RUNNING',
+ LONG_STATUS: 'LONG_STATUS',
+ NEW_ISSUE: 'NEW_ISSUE',
+ NEW_ISSUE_COMMENT: 'NEW_ISSUE_COMMENT',
+ PIPELINE_RUN_STATUS: 'PIPELINE_RUN_STATUS',
+ STORAGE_QUOTA_EXCEEDING: 'STORAGE_QUOTA_EXCEEDING',
+ INACTIVE_USERS: 'INACTIVE_USERS',
+ LDAP_BLOCKED_POSTPONED_USERS: 'LDAP_BLOCKED_POSTPONED_USERS',
+ LDAP_BLOCKED_USERS: 'LDAP_BLOCKED_USERS'
+};
+
+export {ACTIONS, ENTITY_CLASSES, NOTIFICATION_TYPES};
diff --git a/client/src/components/main/notification/notification-actions/index.js b/client/src/components/main/notification/notification-actions/index.js
new file mode 100644
index 0000000000..aa4bda0548
--- /dev/null
+++ b/client/src/components/main/notification/notification-actions/index.js
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2017-2023 EPAM Systems, Inc. (https://www.epam.com/)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import {inject, observer} from 'mobx-react';
+import {computed, observable} from 'mobx';
+import {
+ Dropdown,
+ Icon,
+ Menu,
+ message
+} from 'antd';
+import PipelineRunInfo from '../../../../models/pipelines/PipelineRunInfo';
+import styles from './notification-actions.css';
+import {ACTIONS, ENTITY_CLASSES, NOTIFICATION_TYPES} from './actions';
+
+@inject('preferences')
+@observer
+class NotificationActions extends React.Component {
+ state = {
+ visible: false,
+ pending: false,
+ entityRequestPending: false
+ };
+
+ @observable
+ entityInfoRequest;
+
+ groupedActions = {
+ [NOTIFICATION_TYPES.IDLE_RUN]: [
+ ACTIONS.viewRun,
+ ACTIONS.pauseRun,
+ ACTIONS.stopRun
+ ],
+ [NOTIFICATION_TYPES.IDLE_RUN_PAUSED]: [
+ ACTIONS.viewRun,
+ ACTIONS.resumeRun,
+ ACTIONS.terminateRun
+ ],
+ [NOTIFICATION_TYPES.IDLE_RUN_STOPPED]: [
+ ACTIONS.viewRun
+ ],
+ [NOTIFICATION_TYPES.HIGH_CONSUMED_RESOURCES]: [
+ ACTIONS.viewRun
+ ],
+ [NOTIFICATION_TYPES.LONG_INIT]: [
+ ACTIONS.viewRun,
+ ACTIONS.terminateRun
+ ],
+ [NOTIFICATION_TYPES.LONG_PAUSED]: [
+ ACTIONS.viewRun,
+ ACTIONS.resumeRun,
+ ACTIONS.terminateRun
+ ],
+ [NOTIFICATION_TYPES.LONG_PAUSED_STOPPED]: [
+ ACTIONS.viewRun
+ ],
+ [NOTIFICATION_TYPES.LONG_RUNNING]: [
+ ACTIONS.viewRun,
+ ACTIONS.pauseRun,
+ ACTIONS.stopRun
+ ],
+ [NOTIFICATION_TYPES.LONG_STATUS]: [
+ ACTIONS.viewRun
+ ],
+ [NOTIFICATION_TYPES.PIPELINE_RUN_STATUS]: [
+ ACTIONS.viewRun
+ ],
+ [NOTIFICATION_TYPES.DATASTORAGE_LIFECYCLE_RESTORE_ACTION]: [
+ ACTIONS.openDatastorage
+ ],
+ [NOTIFICATION_TYPES.STORAGE_QUOTA_EXCEEDING]: [
+ ACTIONS.openDatastorage
+ ],
+ [NOTIFICATION_TYPES.DATASTORAGE_LIFECYCLE_ACTION]: [
+ ACTIONS.openDatastorage,
+ ACTIONS.postponeLifecycleRule
+ ],
+ [NOTIFICATION_TYPES.FULL_NODE_POOL]: [
+ ACTIONS.openPoolsUsage
+ ]
+ };
+
+ @computed
+ get actions () {
+ const {preferences} = this.props;
+ const {type} = this.notificationDetails;
+ const actions = this.groupedActions[type] || [];
+ const entityValue = this.entityInfoRequest && this.entityInfoRequest.loaded
+ ? this.entityInfoRequest.value
+ : undefined;
+ return actions
+ .filter(Boolean)
+ .filter(({available}) => available(entityValue, preferences));
+ }
+
+ get notificationDetails () {
+ const {notification = {}} = this.props;
+ const details = (notification.resources || [])[0] || {};
+ return {
+ ...details,
+ type: notification.type
+ };
+ }
+
+ get showActionsControl () {
+ const {type} = this.notificationDetails;
+ return type && type !== NOTIFICATION_TYPES.INACTIVE_USERS &&
+ type !== NOTIFICATION_TYPES.LDAP_BLOCKED_POSTPONED_USERS &&
+ type !== NOTIFICATION_TYPES.LDAP_BLOCKED_USERS;
+ }
+
+ fetchEntityInfo = () => {
+ return new Promise(resolve => {
+ const {entityClass, entityId} = this.notificationDetails;
+ switch (entityClass) {
+ case ENTITY_CLASSES.RUN:
+ this.entityInfoRequest = new PipelineRunInfo(entityId);
+ break;
+ }
+ if (!this.entityInfoRequest) {
+ resolve();
+ return;
+ }
+ this.setState({
+ entityRequestPending: true
+ }, async () => {
+ await this.entityInfoRequest.fetch();
+ if (this.entityInfoRequest.error) {
+ message.error(this.entityInfoRequest.error);
+ }
+ this.setState({
+ entityRequestPending: false
+ }, () => resolve());
+ });
+ });
+ };
+
+ showMenu = () => {
+ this.setState({visible: true});
+ };
+
+ hideMenu = () => {
+ this.setState({visible: false});
+ };
+
+ handleVisibleChange = async (visible) => {
+ if (!visible) {
+ return this.hideMenu();
+ }
+ await this.fetchEntityInfo();
+ this.showMenu();
+ };
+
+ render () {
+ const {
+ pending,
+ notification,
+ style
+ } = this.props;
+ const {visible, entityRequestPending} = this.state;
+ const menu = (
+
+ );
+ return (
+
+ {this.showActionsControl ? (
+ e.stopPropagation()}
+ >
+
+
+ ) : null}
+
+ );
+ }
+}
+
+NotificationActions.propTypes = {
+ style: PropTypes.object,
+ notification: PropTypes.object,
+ router: PropTypes.object
+};
+
+export default NotificationActions;
diff --git a/client/src/components/main/notification/notification-actions/notification-actions.css b/client/src/components/main/notification/notification-actions/notification-actions.css
new file mode 100644
index 0000000000..18d921d28a
--- /dev/null
+++ b/client/src/components/main/notification/notification-actions/notification-actions.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017-2023 EPAM Systems, Inc. (https://www.epam.com/)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.controls-icon {
+ font-size: larger;
+}
diff --git a/client/src/models/dataStorage/lifeCycleRules/DataStorageLifeCycleRulesPostpone.js b/client/src/models/dataStorage/lifeCycleRules/DataStorageLifeCycleRulesPostpone.js
new file mode 100644
index 0000000000..f06bdc3ff0
--- /dev/null
+++ b/client/src/models/dataStorage/lifeCycleRules/DataStorageLifeCycleRulesPostpone.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-2022 EPAM Systems, Inc. (https://www.epam.com/)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Remote from '../../basic/Remote';
+
+const DEFAULT_DAYS = 14;
+
+export default class DataStorageLifeCycleRulesPostpone extends Remote {
+ url;
+
+ constructor ({
+ datastorageId,
+ ruleId,
+ path,
+ days = DEFAULT_DAYS,
+ force = false
+ }) {
+ super();
+ const parts = [
+ path !== undefined && `path=${encodeURIComponent(path)}`,
+ days !== undefined && `days=${days}`,
+ force !== undefined && `force=${force}`
+ ].filter(Boolean);
+ const query = `?${parts.join('&')}`;
+ this.url = `/datastorage/${datastorageId}/lifecycle/rule/${ruleId}/prolong${query}`;
+ }
+}