From de0b07fffd01cc2552c9a4a50ebbc994fb684400 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 26 Jan 2016 14:34:51 +0100 Subject: SONAR-7230 Move user notifications into separate page of "My Account" space --- .../qualityGate/QualityGateNotificationTest.java | 1 - .../activate_notification_channels.html | 11 ++- server/sonar-web/src/main/js/api/components.js | 10 ++ server/sonar-web/src/main/js/apps/account/app.js | 21 +++-- .../apps/account/components/GlobalNotifications.js | 53 +++++++++++ .../src/main/js/apps/account/components/Nav.js | 5 + .../js/apps/account/components/Notifications.js | 41 ++++++++- .../apps/account/components/NotificationsList.js | 42 +++++++++ .../account/components/ProjectNotifications.js | 102 +++++++++++++++++++++ .../account/containers/NotificationsContainer.js | 42 +++++++++ .../src/main/js/apps/account/store/actions.js | 35 +++++++ .../main/js/apps/account/store/configureStore.js | 36 ++++++++ .../src/main/js/apps/account/store/reducers.js | 69 ++++++++++++++ .../WEB-INF/app/controllers/account_controller.rb | 12 +-- .../app/controllers/api/resources_controller.rb | 1 - .../WEB-INF/app/views/account/index.html.erb | 71 ++++++++++++++ .../src/main/webapp/WEB-INF/config/routes.rb | 2 + .../main/resources/org/sonar/l10n/core.properties | 2 + 18 files changed, 529 insertions(+), 27 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js create mode 100644 server/sonar-web/src/main/js/apps/account/components/NotificationsList.js create mode 100644 server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js create mode 100644 server/sonar-web/src/main/js/apps/account/containers/NotificationsContainer.js create mode 100644 server/sonar-web/src/main/js/apps/account/store/actions.js create mode 100644 server/sonar-web/src/main/js/apps/account/store/configureStore.js create mode 100644 server/sonar-web/src/main/js/apps/account/store/reducers.js diff --git a/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java b/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java index 37271141fbe..5920d677138 100644 --- a/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java +++ b/it/it-tests/src/test/java/it/qualityGate/QualityGateNotificationTest.java @@ -70,7 +70,6 @@ public class QualityGateNotificationTest { } @Test - @Ignore("waiting for SONAR-7230") public void status_on_metric_variation_and_send_notifications() throws Exception { Wiser smtpServer = new Wiser(NetworkUtils.getNextAvailablePort()); try { diff --git a/it/it-tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html b/it/it-tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html index b72649cf564..fc1b770918e 100644 --- a/it/it-tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html +++ b/it/it-tests/src/test/resources/qualityGate/notifications/activate_notification_channels.html @@ -35,17 +35,22 @@ open - /account/index + /sonar/account/notifications + + + + waitForElementPresent + id=global_notifs_NewAlerts_EmailNotificationChannel check - global_notifs_NewAlerts.EmailNotificationChannel + id=global_notifs_NewAlerts_EmailNotificationChannel clickAndWait - //input[@value='Save changes'] + id=submit-notifications diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js index 89a6e35ffec..32ed9f744cd 100644 --- a/server/sonar-web/src/main/js/api/components.js +++ b/server/sonar-web/src/main/js/api/components.js @@ -91,3 +91,13 @@ export function getBreadcrumbs ({ id, key }) { return [...reversedAncestors, r.component]; }); } + +export function getProjectsWithInternalId (query) { + const url = window.baseUrl + '/api/resources/search'; + const data = { + f: 's2', + q: 'TRK', + s: query + }; + return getJSON(url, data).then(r => r.results); +} diff --git a/server/sonar-web/src/main/js/apps/account/app.js b/server/sonar-web/src/main/js/apps/account/app.js index 5af38f5b6c3..4a78a6a04fb 100644 --- a/server/sonar-web/src/main/js/apps/account/app.js +++ b/server/sonar-web/src/main/js/apps/account/app.js @@ -21,10 +21,12 @@ import React from 'react'; import { render } from 'react-dom'; import { Router, Route, IndexRoute, Redirect } from 'react-router'; import { createHistory, useBasename } from 'history'; +import { Provider } from 'react-redux'; +import configureStore from './store/configureStore'; import AccountApp from './containers/AccountApp'; import Home from './components/Home'; -import Notifications from './components/Notifications'; +import NotificationsContainer from './containers/NotificationsContainer'; window.sonarqube.appStarted.then(options => { const el = document.querySelector(options.el); @@ -33,16 +35,21 @@ window.sonarqube.appStarted.then(options => { basename: window.baseUrl + '/account' }); + const store = configureStore(); + document.querySelector('html').classList.add('dashboard-page'); document.querySelector('#container').classList.add('page-wrapper-context'); render(( - - - + + + + + - - - + + + + ), el); }); diff --git a/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js b/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js new file mode 100644 index 00000000000..5f5de5de9b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/components/GlobalNotifications.js @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import NotificationsList from './NotificationsList'; +import { translate } from '../../../helpers/l10n'; + +export default function GlobalNotifications ({ notifications, channels }) { + return ( +
+
+

+ {translate('my_profile.overall_notifications.title')} +

+
+ + + + + + {channels.map(channel => ( + + ))} + + + + `global_notifs_${d}_${c}`} + checkboxName={(d, c) => `global_notifs[${d}.${c}]`}/> +
+

{translate('notification.channel', channel)}

+
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/account/components/Nav.js b/server/sonar-web/src/main/js/apps/account/components/Nav.js index 3a85c7da307..e19213d3062 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Nav.js +++ b/server/sonar-web/src/main/js/apps/account/components/Nav.js @@ -38,6 +38,11 @@ const Nav = () => ( +
  • + + {translate('my_account.notifications')} + +
  • diff --git a/server/sonar-web/src/main/js/apps/account/components/Notifications.js b/server/sonar-web/src/main/js/apps/account/components/Notifications.js index 195a134c15c..45a9d5839eb 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Notifications.js +++ b/server/sonar-web/src/main/js/apps/account/components/Notifications.js @@ -19,8 +19,41 @@ */ import React from 'react'; -const Notifications = () => ( -

    Notifications

    -); +import GlobalNotifications from './GlobalNotifications'; +import ProjectNotifications from './ProjectNotifications'; +import { translate } from '../../../helpers/l10n'; -export default Notifications; +export default function Notifications ({ globalNotifications, projectNotifications, onAddProject, onRemoveProject }) { + const channels = globalNotifications[0].channels.map(c => c.id); + + return ( +
    +

    + {translate('notification.dispatcher.information')} +

    +
    +
    +
    + +
    + +
    + +
    +
    + +

    + +

    +
    +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js b/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js new file mode 100644 index 00000000000..a8044b4b142 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/components/NotificationsList.js @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; + +import { translate } from '../../../helpers/l10n'; + +export default function NotificationsList ({ notifications, checkboxName, checkboxId }) { + return ( + + {notifications.map(notification => ( + + {translate('notification.dispatcher', notification.dispatcher)} + {notification.channels.map(channel => ( + + + + ))} + + ))} + + ); +} diff --git a/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js b/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js new file mode 100644 index 00000000000..5683e50e1d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/components/ProjectNotifications.js @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import Select from 'react-select'; + +import NotificationsList from './NotificationsList'; +import { translate } from '../../../helpers/l10n'; +import { getProjectsWithInternalId } from '../../../api/components'; + +export default function ProjectNotifications ({ notifications, channels, onAddProject, onRemoveProject }) { + const loadOptions = (query) => { + return getProjectsWithInternalId(query) + .then(results => results.map(r => { + return { + value: r.id, + label: r.text + }; + })) + .then(options => { + return { options }; + }); + }; + + const handleAddProject = (selected) => { + const project = { + internalId: selected.value, + name: selected.label + }; + onAddProject(project); + }; + + const handleRemoveProject = (project) => ( + (e) => { + e.preventDefault; + onRemoveProject(project); + } + ); + + return ( +
    +
    +

    + {translate('my_profile.per_project_notifications.title')} +

    +
    + +
    +
    + + {!notifications.length && ( +
    + {translate('my_account.no_project_notifications')} +
    + )} + + {notifications.map(p => ( + + + + + {channels.map(channel => ( + + ))} + + + `project_notifs_${p.project.internalId}_${d}_${c}`} + checkboxName={(d, c) => `project_notifs[${p.project.internalId}][${d}][${c}]`}/> +
    + +

    {p.project.name}

    +
    +

    {translate('notification.channel', channel)}

    +
    + ))} +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/account/containers/NotificationsContainer.js b/server/sonar-web/src/main/js/apps/account/containers/NotificationsContainer.js new file mode 100644 index 00000000000..84328295905 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/containers/NotificationsContainer.js @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { connect } from 'react-redux'; + +import { addProjectNotifications, removeProjectNotifications } from '../store/actions'; +import Notifications from './../components/Notifications'; + +function mapStateToProps (state) { + return { + globalNotifications: window.sonarqube.notifications.global, + projectNotifications: state.projectNotifications + }; +} + +function mapDispatchToProps (dispatch) { + return { + onAddProject: (project) => dispatch(addProjectNotifications(project)), + onRemoveProject: (project) => dispatch(removeProjectNotifications(project)) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Notifications); diff --git a/server/sonar-web/src/main/js/apps/account/store/actions.js b/server/sonar-web/src/main/js/apps/account/store/actions.js new file mode 100644 index 00000000000..bee94192a5c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/store/actions.js @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const ADD_PROJECT_NOTIFICATIONS = 'ADD_PROJECT_NOTIFICATIONS'; +export const REMOVE_PROJECT_NOTIFICATIONS = 'REMOVE_PROJECT_NOTIFICATIONS'; + +export function addProjectNotifications (project) { + return { + type: ADD_PROJECT_NOTIFICATIONS, + project + }; +} + +export function removeProjectNotifications (project) { + return { + type: REMOVE_PROJECT_NOTIFICATIONS, + project + }; +} diff --git a/server/sonar-web/src/main/js/apps/account/store/configureStore.js b/server/sonar-web/src/main/js/apps/account/store/configureStore.js new file mode 100644 index 00000000000..e412913dc76 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/store/configureStore.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import createLogger from 'redux-logger'; +import rootReducer from './reducers'; + +const logger = createLogger({ + predicate: () => process.env.NODE_ENV !== 'production' +}); + +const createStoreWithMiddleware = applyMiddleware( + thunk, + logger +)(createStore); + +export default function configureStore () { + return createStoreWithMiddleware(rootReducer); +} diff --git a/server/sonar-web/src/main/js/apps/account/store/reducers.js b/server/sonar-web/src/main/js/apps/account/store/reducers.js new file mode 100644 index 00000000000..23921840d8b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/store/reducers.js @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { ADD_PROJECT_NOTIFICATIONS, REMOVE_PROJECT_NOTIFICATIONS } from './actions'; + +function addProjectNotifications (state, project) { + const found = state.find(notification => { + return notification.project.internalId === project.internalId; + }); + + if (found) { + return state; + } + + const newProjectNotification = { + project, + notifications: window.sonarqube.notifications.projectDispatchers.map(dispatcher => { + const channels = window.sonarqube.notifications.channels.map(channel => { + return { id: channel, checked: false }; + }); + return { dispatcher, channels }; + }) + }; + + return [...state, newProjectNotification]; +} + +function removeProjectNotifications (state, project) { + return state.filter(notification => { + return notification.project.internalId !== project.internalId; + }); +} + +export const initialState = { + projectNotifications: window.sonarqube.notifications.project +}; + +export default function (state = initialState, action) { + switch (action.type) { + case ADD_PROJECT_NOTIFICATIONS: + return { + ...state, + projectNotifications: addProjectNotifications(state.projectNotifications, action.project) + }; + case REMOVE_PROJECT_NOTIFICATIONS: + return { + ...state, + projectNotifications: removeProjectNotifications(state.projectNotifications, action.project) + }; + default: + return state; + } +} diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb index aa803c1f9b9..508c5ac7ae1 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/account_controller.rb @@ -22,10 +22,6 @@ class AccountController < ApplicationController before_filter :login_required def index - - end - - def notifications @channels = notification_service.getChannels() @global_dispatchers = dispatchers_for_scope("globalNotification") @per_project_dispatchers = dispatchers_for_scope("perProjectNotification") @@ -84,13 +80,7 @@ class AccountController < ApplicationController end end - # New project added - new_params = {} - unless params[:new_project].blank? - new_params[:new_project] = params[:new_project] - end - - redirect_to :action => 'index', :params => new_params + redirect_to "#{ApplicationController.root_context}/account/notifications" end private diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb index b85e5c7adbb..64da4002f0c 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/resources_controller.rb @@ -39,7 +39,6 @@ class Api::ResourcesController < Api::ApiController qualifiers=[] end - bad_request("Minimum search is #{ResourceIndex::MIN_SEARCH_SIZE} characters") if search_text.size ] }; + window.sonarqube.notifications = { + channels: [ + <% for channel in @channels -%> + '<%= escape_javascript channel.getKey() -%>', + <% end %> + ], + + globalDispatchers: [ + <% for dispatcher in @global_dispatchers -%> + '<%= escape_javascript dispatcher -%>', + <% end %> + ], + + projectDispatchers: [ + <% for dispatcher in @per_project_dispatchers -%> + '<%= escape_javascript dispatcher -%>', + <% end %> + ], + + global: [ + <% for dispatcher in @global_dispatchers %> + { + dispatcher: '<%= escape_javascript dispatcher -%>', + channels: [ + <% + for channel in @channels + notification_id = dispatcher + '.' + channel.getKey() + check_box_checked = @global_notifications[notification_id] + -%> + { + id: '<%= escape_javascript channel.getKey() -%>', + checked: <%= check_box_checked ? 'true' : 'false' %> + }, + <% end %> + ] + }, + <% end %> + ], + + project: [ + <% @per_project_notifications.each do |project_key, notification| %> + <% project = Project.by_key(project_key) %> + { + project: { + internalId: <%= project.id -%>, + id: '<%= escape_javascript project.uuid -%>', + key: '<%= escape_javascript project.key -%>', + name: '<%= escape_javascript project.name -%>' + }, + notifications: [ + <% @per_project_dispatchers.each do |dispatcher| %> + { + dispatcher: '<%= escape_javascript dispatcher -%>', + channels: [ + <% + for channel in @channels + check_box_checked = notification[dispatcher].include?(channel.getKey()) + -%> + { + id: '<%= escape_javascript channel.getKey() -%>', + checked: <%= check_box_checked ? 'true' : 'false' %> + }, + <% end %> + ] + }, + <% end %> + ] + }, + <% end %> + ] + }; <% end %> diff --git a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb index 36e2931ee2f..69814d108fb 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb @@ -34,6 +34,8 @@ ActionController::Routing::Routes.draw do |map| map.connect 'api_documentation/*other', :controller => 'api_documentation', :action => 'index' map.connect 'quality_gates/*other', :controller => 'quality_gates', :action => 'index' map.connect 'overview/*other', :controller => 'overview', :action => 'index' + map.connect 'account/update_notifications', :controller => 'account', :action => 'update_notifications' + map.connect 'account/*other', :controller => 'account', :action => 'index' # Install the default route as the lowest priority. map.connect ':controller/:action/:id', :requirements => { :id => /.*/ } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index c8f3f6da8fd..fad0339a29d 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2159,6 +2159,8 @@ my_account.page=My Account my_account.favorite_components=Favorite Components my_account.favorite_issue_filters=Favorite Issue Filters my_account.favorite_measure_filters=Favorite Measure Filters +my_account.notifications=Notifications +my_account.no_project_notifications=You have not set project notifications yet. -- cgit v1.2.3