Explorar el Código

ux: org user management changes

Torkel Ödegaard hace 8 años
padre
commit
ccbd18006e

+ 2 - 1
pkg/api/api.go

@@ -40,7 +40,8 @@ func (hs *HttpServer) registerRoutes() {
 	r.Get("/datasources/", reqSignedIn, Index)
 	r.Get("/datasources/new", 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("/dashboard/import/", reqSignedIn, Index)
 	r.Get("/configuration", reqGrafanaAdmin, Index)

+ 1 - 1
pkg/api/dtos/invite.go

@@ -6,7 +6,7 @@ type AddInviteForm struct {
 	LoginOrEmail string     `json:"loginOrEmail" binding:"Required"`
 	Name         string     `json:"name"`
 	Role         m.RoleType `json:"role" binding:"Required"`
-	SkipEmails   bool       `json:"skipEmails"`
+	SendEmail    bool       `json:"sendEmail"`
 }
 
 type InviteInfo struct {

+ 2 - 2
pkg/api/org_invite.go

@@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
 	}
 
 	// send invite email
-	if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) {
+	if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
 		emailCmd := m.SendEmailCommand{
 			To:       []string{inviteDto.LoginOrEmail},
 			Template: "new_user_invite.html",
@@ -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)
 	} else {
 
-		if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
+		if inviteDto.SendEmail && util.IsEmail(user.Email) {
 			emailCmd := m.SendEmailCommand{
 				To:       []string{user.Email},
 				Template: "invited_to_org.html",

+ 1 - 90
public/app/core/nav_model_srv.ts

@@ -27,9 +27,8 @@ export class NavModel {
 export class NavModelSrv {
   navItems: any;
 
-
   /** @ngInject */
-  constructor(private contextSrv) {
+  constructor() {
     this.navItems = config.bootData.navTree;
   }
 
@@ -81,94 +80,6 @@ export class NavModelSrv {
       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);

+ 2 - 1
public/app/core/routes/routes.ts

@@ -109,9 +109,10 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     controllerAs: 'ctrl',
     resolve: loadOrgBundle,
   })
-  .when('/org/users/new', {
+  .when('/org/users/invite', {
     templateUrl: 'public/app/features/org/partials/invite.html',
     controller : 'UserInviteCtrl',
+    controllerAs: 'ctrl',
     resolve: loadOrgBundle,
   })
   .when('/org/apikeys', {

+ 19 - 39
public/app/features/org/org_users_ctrl.ts

@@ -1,10 +1,10 @@
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import Remarkable from 'remarkable';
+import _ from 'lodash';
 
 export class OrgUsersCtrl {
-
-  user: any;
+  unfiltered: any;
   users: any;
   pendingInvites: any;
   editor: any;
@@ -12,21 +12,18 @@ export class OrgUsersCtrl {
   externalUserMngLinkUrl: string;
   externalUserMngLinkName: string;
   externalUserMngInfo: string;
-  addUsersBtnName: string;
+  canInvite: boolean;
+  searchQuery: string;
+  showInvites: boolean;
 
   /** @ngInject */
   constructor(private $scope, private backendSrv, navModelSrv, $sce) {
-    this.user = {
-      loginOrEmail: '',
-      role: 'Viewer',
-    };
-
     this.navModel = navModelSrv.getNav('cfg', 'users', 0);
 
     this.get();
-    this.editor = { index: 0 };
     this.externalUserMngLinkUrl = config.externalUserMngLinkUrl;
     this.externalUserMngLinkName = config.externalUserMngLinkName;
+    this.canInvite = !config.disableLoginForm && !config.externalUserMngLinkName;
 
     // render external user management info markdown
     if (config.externalUserMngInfo) {
@@ -34,21 +31,13 @@ export class OrgUsersCtrl {
         linkTarget: '__blank',
       }).render(config.externalUserMngInfo);
     }
-
-    this.addUsersBtnName = this.getAddUserBtnName();
-  }
-
-  getAddUserBtnName(): string {
-    if (this.externalUserMngLinkName) {
-      return this.externalUserMngLinkName;
-    }
-    return "Invite User";
   }
 
   get() {
     this.backendSrv.get('/api/org/users')
       .then((users) => {
         this.users = users;
+        this.unfiltered = users;
       });
     this.backendSrv.get('/api/org/invites')
       .then((pendingInvites) => {
@@ -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) {
     this.backendSrv.patch('/api/org/users/' + user.userId, user);
   }
@@ -74,38 +70,22 @@ export class OrgUsersCtrl {
 
   removeUserConfirmed(user) {
     this.backendSrv.delete('/api/org/users/' + user.userId)
-      .then(this.get.bind(this));
+    .then(this.get.bind(this));
   }
 
   revokeInvite(invite, evt) {
     evt.stopPropagation();
     this.backendSrv.patch('/api/org/invites/' + invite.code + '/revoke')
-      .then(this.get.bind(this));
+    .then(this.get.bind(this));
   }
 
   copyInviteToClipboard(evt) {
     evt.stopPropagation();
   }
 
- getInviteUrl(invite) {
-   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
-     });
- }
-
+  getInviteUrl(invite) {
+    return invite.url;
+  }
 }
 
 coreModule.controller('OrgUsersCtrl', OrgUsersCtrl);

+ 0 - 55
public/app/features/org/partials/add_user.html

@@ -1,55 +0,0 @@
-<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>

+ 29 - 43
public/app/features/org/partials/invite.html

@@ -1,49 +1,35 @@
-<page-header model="navModel"></page-header>
+<page-header model="ctrl.navModel"></page-header>
 
 <div class="page-container page-body" ng-cloak>
-  <div class="p-b-2">
-    Send invite or 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-14">
-          <span class="gf-form-label">Name</span>
-          <input type="text" ng-model="invite.name" class="gf-form-input" placeholder="name (optional)">
-        </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>
+	<h2 class="page-sub-heading">Invite User</h2>
 
-    <div class="gf-form-inline gf-form-group">
-      <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="p-b-2">
+		Send invite or add existing Grafana user to the organization
+		<span class="highlight-word">{{contextSrv.user.orgName}}</span>
+	</div>
 
-    <div class="gf-form-button-row">
-      <button type="submit" class="btn btn-success" ng-click="sendInvites();">Invite Users</button>
-      <a class="btn-text" href="org/users">Cancel</a>
-    </div>
-    <div class="clearfix"></div>
-  </form>
+	<form name="ctrl.inviteForm">
+		<div class="gf-form-group">
+			<div class="gf-form max-width-30">
+				<span class="gf-form-label width-10">Email or Username</span>
+				<input type="text" ng-model="ctrl.invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
+			</div>
+			<div class="gf-form max-width-30">
+				<span class="gf-form-label width-10">Name</span>
+				<input type="text" ng-model="ctrl.invite.name" class="gf-form-input" placeholder="name (optional)">
+			</div>
+			<div class="gf-form max-width-30">
+				<span class="gf-form-label width-10">Role</span>
+				<select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
+				</select>
+			</div>
+
+			<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-button-row">
+				<button type="submit" class="btn btn-success" ng-click="ctrl.sendInvite();">Invite</button>
+				<a class="btn btn-inverse" href="org/users">Back</a>
+			</div>
+	</form>
 </div>

+ 31 - 69
public/app/features/org/partials/orgUsers.html

@@ -1,50 +1,26 @@
-<!-- <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>
 
 <div class="page-container page-body">
   <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>
-    <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}})
     </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>
-			<span>{{ctrl.addUsersBtnName}}</span>
+      <span>Invite</span>
     </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>
-      {{ctrl.addUsersBtnName}}
+      {{ctrl.externalUserMngLinkName}}
     </a>
   </div>
 
@@ -52,7 +28,7 @@
     <span ng-bind-html="ctrl.externalUserMngInfo"></span>
   </div>
 
-  <div ng-if="ctrl.editor.index === 0" class="tab-content">
+  <div ng-hide="ctrl.showInvites">
     <table class="filter-table form-inline">
       <thead>
         <tr>
@@ -89,46 +65,32 @@
     </table>
   </div>
 
-  <div ng-if="ctrl.editor.index === 1">
+  <div ng-if="ctrl.showInvites">
     <table class="filter-table form-inline">
       <thead>
         <tr>
           <th>Email</th>
           <th>Name</th>
           <th></th>
+          <th style="width: 34px;"></th>
         </tr>
       </thead>
-      <tbody ng-repeat="invite in ctrl.pendingInvites">
-        <tr ng-click="invite.expanded = !invite.expanded" ng-class="{'expanded': invite.expanded}">
-          <td>{{invite.email}}</td>
-          <td>{{invite.name}}</td>
-          <td class="text-right">
-            <button class="btn btn-inverse btn-mini" clipboard-button="ctrl.getInviteUrl(invite)" ng-click="ctrl.copyInviteToClipboard($event)">
-              <i class="fa fa-clipboard"></i> Copy Invite
-            </button>
-            &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>
-        </tr>
-        <tr ng-show="invite.expanded">
-          <td colspan="3">
-            <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>
-            <span style="padding-left: 15px">
-              Invited: <em> {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} </em>
-            </span>
-          </td>
-        </tr>
-      </tbody>
+      <tr ng-repeat="invite in ctrl.pendingInvites">
+        <td>{{invite.email}}</td>
+        <td>{{invite.name}}</td>
+        <td class="text-right">
+          <button class="btn btn-inverse btn-mini" clipboard-button="ctrl.getInviteUrl(invite)" ng-click="ctrl.copyInviteToClipboard($event)">
+            <i class="fa fa-clipboard"></i> Copy Invite
+          </button>
+          &nbsp;
+        </td>
+        <td>
+          <button class="btn btn-danger btn-mini" ng-click="ctrl.revokeInvite(invite, $event)">
+            <i class="fa fa-remove"></i>
+          </button>
+        </td>
+      </tr>
     </table>
-
   </div>
 </div>
 

+ 19 - 45
public/app/features/org/user_invite_ctrl.ts

@@ -1,56 +1,30 @@
 import coreModule from 'app/core/core_module';
-import _ from 'lodash';
 
 export class UserInviteCtrl {
+  navModel: any;
+  invite: any;
+  inviteForm: any;
 
   /** @ngInject **/
-  constructor($scope, backendSrv, navModelSrv) {
-    $scope.navModel = navModelSrv.getNav('cfg', 'users', 0);
-
-    const defaultInvites = [
-      {name: '', email: '', role: 'Editor'},
-    ];
-
-    $scope.invites = _.cloneDeep(defaultInvites);
-
-    $scope.options = {skipEmails: false};
-    $scope.init = function() { };
-
-    $scope.addInvite = function() {
-      $scope.invites.push({name: '', email: '', role: 'Editor'});
-    };
-
-    $scope.removeInvite = function(invite) {
-      $scope.invites = _.without($scope.invites, invite);
-    };
-
-    $scope.resetInvites = function() {
-      $scope.invites = _.cloneDeep(defaultInvites);
+  constructor(private backendSrv, navModelSrv, private $location) {
+    this.navModel = navModelSrv.getNav('cfg', 'users', 0);
+
+    this.invite = {
+      name: '',
+      email: '',
+      role: 'Editor',
+      sendEmail: true,
     };
+  }
 
-    $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;
+  sendInvite() {
+    if (!this.inviteForm.$valid) {
+      return;
+    }
 
-        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/');
+    });
   }
 }
 

+ 1 - 1
public/app/features/plugins/partials/plugin_list.html

@@ -4,7 +4,7 @@
   <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()" />
+      <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 class="page-action-bar__spacer"></div>

+ 1 - 0
public/sass/layout/_page.scss

@@ -36,6 +36,7 @@
 
 .page-body {
   padding-top: $spacer*2;
+  min-height: 500px;
 }
 
 .page-heading {