Commit ccbd1800 by Torkel Ödegaard

ux: org user management changes

parent cacbcb9c
...@@ -40,7 +40,8 @@ func (hs *HttpServer) registerRoutes() { ...@@ -40,7 +40,8 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index)
r.Get("/datasources/new", reqSignedIn, Index) r.Get("/datasources/new", reqSignedIn, Index)
r.Get("/datasources/edit/*", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index)
r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/users/new", reqSignedIn, Index)
r.Get("/org/users/invite", reqSignedIn, Index)
r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index)
r.Get("/dashboard/import/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index)
r.Get("/configuration", reqGrafanaAdmin, Index) r.Get("/configuration", reqGrafanaAdmin, Index)
......
...@@ -6,7 +6,7 @@ type AddInviteForm struct { ...@@ -6,7 +6,7 @@ type AddInviteForm struct {
LoginOrEmail string `json:"loginOrEmail" binding:"Required"` LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
Name string `json:"name"` Name string `json:"name"`
Role m.RoleType `json:"role" binding:"Required"` Role m.RoleType `json:"role" binding:"Required"`
SkipEmails bool `json:"skipEmails"` SendEmail bool `json:"sendEmail"`
} }
type InviteInfo struct { type InviteInfo struct {
......
...@@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response ...@@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
} }
// send invite email // send invite email
if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) { if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
emailCmd := m.SendEmailCommand{ emailCmd := m.SendEmailCommand{
To: []string{inviteDto.LoginOrEmail}, To: []string{inviteDto.LoginOrEmail},
Template: "new_user_invite.html", Template: "new_user_invite.html",
...@@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto ...@@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
return ApiError(500, "Error while trying to create org user", err) return ApiError(500, "Error while trying to create org user", err)
} else { } else {
if !inviteDto.SkipEmails && util.IsEmail(user.Email) { if inviteDto.SendEmail && util.IsEmail(user.Email) {
emailCmd := m.SendEmailCommand{ emailCmd := m.SendEmailCommand{
To: []string{user.Email}, To: []string{user.Email},
Template: "invited_to_org.html", Template: "invited_to_org.html",
......
...@@ -27,9 +27,8 @@ export class NavModel { ...@@ -27,9 +27,8 @@ export class NavModel {
export class NavModelSrv { export class NavModelSrv {
navItems: any; navItems: any;
/** @ngInject */ /** @ngInject */
constructor(private contextSrv) { constructor() {
this.navItems = config.bootData.navTree; this.navItems = config.bootData.navTree;
} }
...@@ -81,94 +80,6 @@ export class NavModelSrv { ...@@ -81,94 +80,6 @@ export class NavModelSrv {
main: node main: node
}; };
} }
getDashboardNav(dashboard, dashNavCtrl) {
// special handling for snapshots
if (dashboard.meta.isSnapshot) {
return {
section: {
title: dashboard.title,
icon: 'icon-gf icon-gf-snapshot'
},
menu: [
{
title: 'Go to original dashboard',
icon: 'fa fa-fw fa-external-link',
url: dashboard.snapshot.originalUrl,
}
]
};
}
var menu = [];
if (dashboard.meta.canEdit) {
menu.push({
title: 'Settings',
icon: 'fa fa-fw fa-cog',
clickHandler: () => dashNavCtrl.openEditView('settings')
});
menu.push({
title: 'Templating',
icon: 'fa fa-fw fa-code',
clickHandler: () => dashNavCtrl.openEditView('templating')
});
menu.push({
title: 'Annotations',
icon: 'fa fa-fw fa-comment',
clickHandler: () => dashNavCtrl.openEditView('annotations')
});
if (!dashboard.meta.isHome) {
menu.push({
title: 'Version history',
icon: 'fa fa-fw fa-history',
clickHandler: () => dashNavCtrl.openEditView('history')
});
}
menu.push({
title: 'View JSON',
icon: 'fa fa-fw fa-eye',
clickHandler: () => dashNavCtrl.viewJson()
});
}
if (this.contextSrv.isEditor && !dashboard.editable) {
menu.push({
title: 'Make Editable',
icon: 'fa fa-fw fa-edit',
clickHandler: () => dashNavCtrl.makeEditable()
});
}
if (this.contextSrv.isEditor && !dashboard.meta.isFolder) {
menu.push({
title: 'Save As...',
icon: 'fa fa-fw fa-save',
clickHandler: () => dashNavCtrl.saveDashboardAs()
});
}
if (dashboard.meta.canSave) {
menu.push({
title: 'Delete',
icon: 'fa fa-fw fa-trash',
clickHandler: () => dashNavCtrl.deleteDashboard()
});
}
return {
section: {
title: dashboard.title,
icon: 'icon-gf icon-gf-dashboard'
},
menu: menu
};
}
} }
coreModule.service('navModelSrv', NavModelSrv); coreModule.service('navModelSrv', NavModelSrv);
...@@ -109,9 +109,10 @@ function setupAngularRoutes($routeProvider, $locationProvider) { ...@@ -109,9 +109,10 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl', controllerAs: 'ctrl',
resolve: loadOrgBundle, resolve: loadOrgBundle,
}) })
.when('/org/users/new', { .when('/org/users/invite', {
templateUrl: 'public/app/features/org/partials/invite.html', templateUrl: 'public/app/features/org/partials/invite.html',
controller : 'UserInviteCtrl', controller : 'UserInviteCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle, resolve: loadOrgBundle,
}) })
.when('/org/apikeys', { .when('/org/apikeys', {
......
import config from 'app/core/config'; import config from 'app/core/config';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import Remarkable from 'remarkable'; import Remarkable from 'remarkable';
import _ from 'lodash';
export class OrgUsersCtrl { export class OrgUsersCtrl {
unfiltered: any;
user: any;
users: any; users: any;
pendingInvites: any; pendingInvites: any;
editor: any; editor: any;
...@@ -12,21 +12,18 @@ export class OrgUsersCtrl { ...@@ -12,21 +12,18 @@ export class OrgUsersCtrl {
externalUserMngLinkUrl: string; externalUserMngLinkUrl: string;
externalUserMngLinkName: string; externalUserMngLinkName: string;
externalUserMngInfo: string; externalUserMngInfo: string;
addUsersBtnName: string; canInvite: boolean;
searchQuery: string;
showInvites: boolean;
/** @ngInject */ /** @ngInject */
constructor(private $scope, private backendSrv, navModelSrv, $sce) { constructor(private $scope, private backendSrv, navModelSrv, $sce) {
this.user = {
loginOrEmail: '',
role: 'Viewer',
};
this.navModel = navModelSrv.getNav('cfg', 'users', 0); this.navModel = navModelSrv.getNav('cfg', 'users', 0);
this.get(); this.get();
this.editor = { index: 0 };
this.externalUserMngLinkUrl = config.externalUserMngLinkUrl; this.externalUserMngLinkUrl = config.externalUserMngLinkUrl;
this.externalUserMngLinkName = config.externalUserMngLinkName; this.externalUserMngLinkName = config.externalUserMngLinkName;
this.canInvite = !config.disableLoginForm && !config.externalUserMngLinkName;
// render external user management info markdown // render external user management info markdown
if (config.externalUserMngInfo) { if (config.externalUserMngInfo) {
...@@ -34,21 +31,13 @@ export class OrgUsersCtrl { ...@@ -34,21 +31,13 @@ export class OrgUsersCtrl {
linkTarget: '__blank', linkTarget: '__blank',
}).render(config.externalUserMngInfo); }).render(config.externalUserMngInfo);
} }
this.addUsersBtnName = this.getAddUserBtnName();
}
getAddUserBtnName(): string {
if (this.externalUserMngLinkName) {
return this.externalUserMngLinkName;
}
return "Invite User";
} }
get() { get() {
this.backendSrv.get('/api/org/users') this.backendSrv.get('/api/org/users')
.then((users) => { .then((users) => {
this.users = users; this.users = users;
this.unfiltered = users;
}); });
this.backendSrv.get('/api/org/invites') this.backendSrv.get('/api/org/invites')
.then((pendingInvites) => { .then((pendingInvites) => {
...@@ -56,6 +45,13 @@ export class OrgUsersCtrl { ...@@ -56,6 +45,13 @@ export class OrgUsersCtrl {
}); });
} }
onQueryUpdated() {
let regex = new RegExp(this.searchQuery, 'ig');
this.users = _.filter(this.unfiltered, item => {
return regex.test(item.email) || regex.test(item.login);
});
}
updateOrgUser(user) { updateOrgUser(user) {
this.backendSrv.patch('/api/org/users/' + user.userId, user); this.backendSrv.patch('/api/org/users/' + user.userId, user);
} }
...@@ -90,22 +86,6 @@ export class OrgUsersCtrl { ...@@ -90,22 +86,6 @@ export class OrgUsersCtrl {
getInviteUrl(invite) { getInviteUrl(invite) {
return invite.url; return invite.url;
} }
openAddUsersView() {
var modalScope = this.$scope.$new();
modalScope.invitesSent = this.get.bind(this);
var src = config.disableLoginForm
? 'public/app/features/org/partials/add_user.html'
: 'public/app/features/org/partials/invite.html';
this.$scope.appEvent('show-modal', {
src: src,
modalClass: 'invite-modal',
scope: modalScope
});
}
} }
coreModule.controller('OrgUsersCtrl', OrgUsersCtrl); coreModule.controller('OrgUsersCtrl', OrgUsersCtrl);
<div class="modal-body" ng-controller="UserInviteCtrl" ng-init="init()">
<div class="modal-header">
<h2 class="modal-header-title">
Add Users
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<div class="modal-tagline p-b-2">
Add existing Grafana users to the organization
<span class="highlight-word">{{contextSrv.user.orgName}}</span>
</div>
<form name="inviteForm">
<div class="gf-form-group">
<div class="gf-form-inline" ng-repeat="invite in invites">
<div class="gf-form max-width-21">
<span class="gf-form-label">Email or Username</span>
<input type="text" ng-model="invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
</div>
<div class="gf-form max-width-10">
<span class="gf-form-label">Role</span>
<select ng-model="invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
</select>
</div>
<div class="gf-form gf-size-auto">
<a class="gf-form-label pointer" tabindex="1" ng-click="removeInvite(invite)">
<i class="fa fa-remove"></i>
</a>
</div>
</div>
</div>
<div class="gf-form-inline gf-form-group">
<div class="gf-form">
<a class="btn btn-inverse btn-small" ng-click="addInvite()">
<i class="fa fa-plus"></i>
Add another
</a>
</div>
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-click="sendInvites();">Add Users</button>
<a class="btn-text" ng-click="dismiss()">Cancel</a>
</div>
<div class="clearfix"></div>
</form>
</div>
</div>
<page-header model="navModel"></page-header> <page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body" ng-cloak> <div class="page-container page-body" ng-cloak>
<h2 class="page-sub-heading">Invite User</h2>
<div class="p-b-2"> <div class="p-b-2">
Send invite or add existing Grafana users to the organization Send invite or add existing Grafana user to the organization
<span class="highlight-word">{{contextSrv.user.orgName}}</span> <span class="highlight-word">{{contextSrv.user.orgName}}</span>
</div> </div>
<form name="inviteForm"> <form name="ctrl.inviteForm">
<div class="gf-form-group"> <div class="gf-form-group">
<div class="gf-form-inline" ng-repeat="invite in invites"> <div class="gf-form max-width-30">
<div class="gf-form max-width-21"> <span class="gf-form-label width-10">Email or Username</span>
<span class="gf-form-label">Email or Username</span> <input type="text" ng-model="ctrl.invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
<input type="text" ng-model="invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
</div> </div>
<div class="gf-form max-width-14"> <div class="gf-form max-width-30">
<span class="gf-form-label">Name</span> <span class="gf-form-label width-10">Name</span>
<input type="text" ng-model="invite.name" class="gf-form-input" placeholder="name (optional)"> <input type="text" ng-model="ctrl.invite.name" class="gf-form-input" placeholder="name (optional)">
</div> </div>
<div class="gf-form max-width-10"> <div class="gf-form max-width-30">
<span class="gf-form-label">Role</span> <span class="gf-form-label width-10">Role</span>
<select ng-model="invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']"> <select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
</select> </select>
</div> </div>
<div class="gf-form gf-size-auto">
<a class="gf-form-label pointer" tabindex="1" ng-click="removeInvite(invite)">
<i class="fa fa-remove"></i>
</a>
</div>
</div>
</div>
<div class="gf-form-inline gf-form-group"> <gf-form-switch class="gf-form" label="Send invite email" checked="ctrl.invite.sendEmail" label-class="width-10"></gf-form-switch>
<div class="gf-form" style="margin-right:.25rem">
<a class="btn btn-inverse gf-form-button" ng-click="addInvite()">
<i class="fa fa-plus"></i>
Invite another
</a>
</div>
<gf-form-switch class="gf-form" label="Skip sending invite email" checked="options.skipEmails" switch-class="max-width-6"></gf-form-switch>
</div>
<div class="gf-form-button-row"> <div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-click="sendInvites();">Invite Users</button> <button type="submit" class="btn btn-success" ng-click="ctrl.sendInvite();">Invite</button>
<a class="btn-text" href="org/users">Cancel</a> <a class="btn btn-inverse" href="org/users">Back</a>
</div> </div>
<div class="clearfix"></div>
</form> </form>
</div> </div>
<!-- <navbar model="ctrl.navModel"></navbar> -->
<!-- -->
<!-- <div class="page&#45;container"> -->
<!-- <div class="page&#45;header"> -->
<!-- <page&#45;h1 model="ctrl.navModel"></page&#45;h1> -->
<!-- -->
<!-- <button class="btn btn&#45;success" ng&#45;click="ctrl.openAddUsersView()" ng&#45;hide="ctrl.externalUserMngLinkUrl"> -->
<!-- <span>{{ctrl.addUsersBtnName}}</span> -->
<!-- </button> -->
<!-- -->
<!-- <div class="page&#45;header&#45;tabs"> -->
<!-- -->
<!-- <a class="btn btn&#45;inverse" ng&#45;href="{{ctrl.externalUserMngLinkUrl}}" target="_blank" ng&#45;if="ctrl.externalUserMngLinkUrl"> -->
<!-- <i class="fa fa&#45;external&#45;link&#45;square"></i> -->
<!-- {{ctrl.addUsersBtnName}} -->
<!-- </a> -->
<!-- -->
<!-- <ul class="gf&#45;tabs"> -->
<!-- <li class="gf&#45;tabs&#45;item"> -->
<!-- <a class="gf&#45;tabs&#45;link" ng&#45;click="ctrl.editor.index = 0" ng&#45;class="{active: ctrl.editor.index === 0}"> -->
<!-- Users ({{ctrl.users.length}}) -->
<!-- </a> -->
<!-- </li> -->
<!-- <li class="gf&#45;tabs&#45;item" ng&#45;show="ctrl.pendingInvites.length"> -->
<!-- <a class="gf&#45;tabs&#45;link" ng&#45;click="ctrl.editor.index = 1" ng&#45;class="{active: ctrl.editor.index === 1}"> -->
<!-- Pending Invites ({{ctrl.pendingInvites.length}}) -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </div> -->
<!-- </div> -->
<page-header model="ctrl.navModel"></page-header> <page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body"> <div class="page-container page-body">
<div class="page-action-bar"> <div class="page-action-bar">
<div class="gf-form">
<label class="gf-form-label">Search</label>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" give-focus="true" placeholder="Filter by username or email" />
</div>
<div class="page-action-bar__spacer"></div> <div class="page-action-bar__spacer"></div>
<button class="btn btn-inverse" ng-show="ctrl.pendingInvites.length" ng-click="ctrl.editor.index = 1">
<button class="btn btn-inverse" ng-show="ctrl.pendingInvites.length" ng-click="ctrl.showInvites = true">
Pending Invites ({{ctrl.pendingInvites.length}}) Pending Invites ({{ctrl.pendingInvites.length}})
</button> </button>
<a class="btn btn-success" href="org/users/new" ng-hide="ctrl.externalUserMngLinkUrl">
<a class="btn btn-success" href="org/users/invite" ng-show="ctrl.canInvite">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
<span>{{ctrl.addUsersBtnName}}</span> <span>Invite</span>
</a> </a>
<a class="btn btn-inverse" ng-href="{{ctrl.externalUserMngLinkUrl}}" target="_blank" ng-if="ctrl.externalUserMngLinkUrl">
<a class="btn btn-success" ng-href="{{ctrl.externalUserMngLinkUrl}}" target="_blank" ng-if="ctrl.externalUserMngLinkUrl">
<i class="fa fa-external-link-square"></i> <i class="fa fa-external-link-square"></i>
{{ctrl.addUsersBtnName}} {{ctrl.externalUserMngLinkName}}
</a> </a>
</div> </div>
...@@ -52,7 +28,7 @@ ...@@ -52,7 +28,7 @@
<span ng-bind-html="ctrl.externalUserMngInfo"></span> <span ng-bind-html="ctrl.externalUserMngInfo"></span>
</div> </div>
<div ng-if="ctrl.editor.index === 0" class="tab-content"> <div ng-hide="ctrl.showInvites">
<table class="filter-table form-inline"> <table class="filter-table form-inline">
<thead> <thead>
<tr> <tr>
...@@ -89,17 +65,17 @@ ...@@ -89,17 +65,17 @@
</table> </table>
</div> </div>
<div ng-if="ctrl.editor.index === 1"> <div ng-if="ctrl.showInvites">
<table class="filter-table form-inline"> <table class="filter-table form-inline">
<thead> <thead>
<tr> <tr>
<th>Email</th> <th>Email</th>
<th>Name</th> <th>Name</th>
<th></th> <th></th>
<th style="width: 34px;"></th>
</tr> </tr>
</thead> </thead>
<tbody ng-repeat="invite in ctrl.pendingInvites"> <tr ng-repeat="invite in ctrl.pendingInvites">
<tr ng-click="invite.expanded = !invite.expanded" ng-class="{'expanded': invite.expanded}">
<td>{{invite.email}}</td> <td>{{invite.email}}</td>
<td>{{invite.name}}</td> <td>{{invite.name}}</td>
<td class="text-right"> <td class="text-right">
...@@ -107,28 +83,14 @@ ...@@ -107,28 +83,14 @@
<i class="fa fa-clipboard"></i> Copy Invite <i class="fa fa-clipboard"></i> Copy Invite
</button> </button>
&nbsp; &nbsp;
<button class="btn btn-inverse btn-mini">
Details
<i ng-show="!invite.expanded" class="fa fa-caret-right"></i>
<i ng-show="invite.expanded" class="fa fa-caret-down"></i>
</button>
</td> </td>
</tr> <td>
<tr ng-show="invite.expanded"> <button class="btn btn-danger btn-mini" ng-click="ctrl.revokeInvite(invite, $event)">
<td colspan="3"> <i class="fa fa-remove"></i>
<a href="{{invite.url}}">{{invite.url}}</a><br><br>
&nbsp;
<button class="btn btn-inverse btn-mini" ng-click="ctrl.revokeInvite(invite, $event)">
<i class="fa fa-remove" style="color: red"></i> Revoke invite
</button> </button>
<span style="padding-left: 15px">
Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
</span>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import _ from 'lodash';
export class UserInviteCtrl { export class UserInviteCtrl {
navModel: any;
invite: any;
inviteForm: any;
/** @ngInject **/ /** @ngInject **/
constructor($scope, backendSrv, navModelSrv) { constructor(private backendSrv, navModelSrv, private $location) {
$scope.navModel = navModelSrv.getNav('cfg', 'users', 0); this.navModel = navModelSrv.getNav('cfg', 'users', 0);
const defaultInvites = [ this.invite = {
{name: '', email: '', role: 'Editor'}, name: '',
]; email: '',
role: 'Editor',
$scope.invites = _.cloneDeep(defaultInvites); sendEmail: true,
$scope.options = {skipEmails: false};
$scope.init = function() { };
$scope.addInvite = function() {
$scope.invites.push({name: '', email: '', role: 'Editor'});
}; };
}
$scope.removeInvite = function(invite) { sendInvite() {
$scope.invites = _.without($scope.invites, invite); if (!this.inviteForm.$valid) {
}; return;
$scope.resetInvites = function() {
$scope.invites = _.cloneDeep(defaultInvites);
};
$scope.sendInvites = function() {
if (!$scope.inviteForm.$valid) { return; }
$scope.sendSingleInvite(0);
};
$scope.invitesSent = function() {
$scope.resetInvites();
};
$scope.sendSingleInvite = function(index) {
var invite = $scope.invites[index];
invite.skipEmails = $scope.options.skipEmails;
return backendSrv.post('/api/org/invites', invite).finally(function() {
index += 1;
if (index === $scope.invites.length) {
$scope.invitesSent();
} else {
$scope.sendSingleInvite(index);
} }
return this.backendSrv.post('/api/org/invites', this.invite).then(() => {
this.$location.path('org/users/');
}); });
};
} }
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="page-action-bar"> <div class="page-action-bar">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label">Search</label> <label class="gf-form-label">Search</label>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" /> <input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by name or type" />
</div> </div>
<div class="page-action-bar__spacer"></div> <div class="page-action-bar__spacer"></div>
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
.page-body { .page-body {
padding-top: $spacer*2; padding-top: $spacer*2;
min-height: 500px;
} }
.page-heading { .page-heading {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment