mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 15:27:47 -02:30
AC-1294 reworked standard out page to support custom scroll bar. Removed use of <pre> elment by parsing the inbound HTML and isolating the style sheet and the job output. Those are now inserted into styled <div> elements, giving the UI better control. The page now listens to the web socket server and refreshes whenver an event happens for the job. On an event the <div> element refreshes and automatically scrolls to the bottom.
This commit is contained in:
@@ -46,6 +46,8 @@ var $AnsibleConfig = {
|
|||||||
gutters: ["CodeMirror-lint-markers"],
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
lint: true
|
lint: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
websocket_port: 8090
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -7,23 +7,44 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors) {
|
function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) {
|
||||||
|
|
||||||
ClearScope();
|
ClearScope();
|
||||||
|
|
||||||
var available_height, job_id = $routeParams.id;
|
var available_height, job_id = $routeParams.id,
|
||||||
|
api_complete = false,
|
||||||
|
stdout_url,
|
||||||
|
event_socket = Socket({
|
||||||
|
scope: $scope,
|
||||||
|
endpoint: "job_events"
|
||||||
|
});
|
||||||
|
|
||||||
Wait('start');
|
Wait('start');
|
||||||
|
|
||||||
|
event_socket.init();
|
||||||
|
|
||||||
|
event_socket.on("job_events-" + job_id, function() {
|
||||||
|
if (api_complete) {
|
||||||
|
$scope.$emit('LoadStdout');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ($scope.removeLoadStdout) {
|
if ($scope.removeLoadStdout) {
|
||||||
$scope.removeLoadStdout();
|
$scope.removeLoadStdout();
|
||||||
}
|
}
|
||||||
$scope.removeLoadStdout = $scope.$on('LoadStdout', function(e, url) {
|
$scope.removeLoadStdout = $scope.$on('LoadStdout', function() {
|
||||||
Rest.setUrl(url + '?format=html');
|
Rest.setUrl(stdout_url + '?format=html');
|
||||||
Rest.get()
|
Rest.get()
|
||||||
.success(function(data) {
|
.success(function(data) {
|
||||||
|
api_complete = true;
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
$('#stdout-iframe').attr('srcdoc', data);
|
var doc, style, pre, parser = new DOMParser();
|
||||||
|
doc = parser.parseFromString(data, "text/html");
|
||||||
|
pre = doc.getElementsByTagName('pre');
|
||||||
|
style = doc.getElementsByTagName('style');
|
||||||
|
$('#style-sheet-container').empty().html(style[0]);
|
||||||
|
$('#pre-container-content').empty().html($(pre[0]).html());
|
||||||
|
setTimeout(function() { $('#pre-container').mCustomScrollbar("scrollTo", 'bottom'); }, 1000);
|
||||||
})
|
})
|
||||||
.error(function(data, status) {
|
.error(function(data, status) {
|
||||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
@@ -33,15 +54,15 @@ function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBas
|
|||||||
|
|
||||||
function resizeToFit() {
|
function resizeToFit() {
|
||||||
available_height = $(window).height() - $('.main-menu').outerHeight() - $('#main_tabs').outerHeight() -
|
available_height = $(window).height() - $('.main-menu').outerHeight() - $('#main_tabs').outerHeight() -
|
||||||
$('#breadcrumb-container').outerHeight() - $('.site-footer').outerHeight();
|
$('#breadcrumb-container').outerHeight() - $('.site-footer').outerHeight() * 2;
|
||||||
if ($(window).width() < 768) {
|
if ($(window).width() < 768) {
|
||||||
available_height += 55;
|
available_height += 55;
|
||||||
}
|
}
|
||||||
else {
|
else if ($(window).width() > 1240) {
|
||||||
available_height += 5;
|
available_height += 5;
|
||||||
}
|
}
|
||||||
$('#stdout-iframe').height(available_height);
|
$('#pre-container').height(available_height);
|
||||||
//$('#stdout-container').mCustomScrollbar("update");
|
$('#pre-container').mCustomScrollbar("update");
|
||||||
}
|
}
|
||||||
resizeToFit();
|
resizeToFit();
|
||||||
|
|
||||||
@@ -53,7 +74,8 @@ function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBas
|
|||||||
Rest.get()
|
Rest.get()
|
||||||
.success(function(data) {
|
.success(function(data) {
|
||||||
$scope.job = data;
|
$scope.job = data;
|
||||||
$scope.$emit('LoadStdout', data.related.stdout);
|
stdout_url = data.related.stdout;
|
||||||
|
$scope.$emit('LoadStdout');
|
||||||
})
|
})
|
||||||
.error(function(data, status) {
|
.error(function(data, status) {
|
||||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
|
||||||
@@ -61,4 +83,4 @@ function JobStdoutController ($scope, $compile, $routeParams, ClearScope, GetBas
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
JobStdoutController.$inject = [ '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors' ];
|
JobStdoutController.$inject = [ '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', 'Socket' ];
|
||||||
@@ -348,6 +348,12 @@ td.actions {
|
|||||||
font-family: Fixed, monospace;
|
font-family: Fixed, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make a div or any element behave like pre. Use in conjunction with .mono-space */
|
||||||
|
.pre {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dd {
|
dd {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
@@ -1658,7 +1664,8 @@ tr td button i {
|
|||||||
|
|
||||||
/* job stdout */
|
/* job stdout */
|
||||||
|
|
||||||
#stdout-iframe {
|
#pre-container {
|
||||||
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
|
|||||||
host = $location.host(),
|
host = $location.host(),
|
||||||
endpoint = params.endpoint,
|
endpoint = params.endpoint,
|
||||||
protocol = $location.protocol(),
|
protocol = $location.protocol(),
|
||||||
url = protocol + '://' + host + ':8080/socket.io/' + endpoint;
|
url = protocol + '://' + host + ':' + $AnsibleConfig.websocket_port + '/socket.io/' + endpoint;
|
||||||
|
|
||||||
function getSocketTip(status) {
|
function getSocketTip(status) {
|
||||||
var result = '';
|
var result = '';
|
||||||
|
|||||||
@@ -722,6 +722,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
|||||||
|
|
||||||
.directive('awCustomScroll', [ function() {
|
.directive('awCustomScroll', [ function() {
|
||||||
return function(scope, element, attrs) {
|
return function(scope, element, attrs) {
|
||||||
|
var theme = (attrs.scrollTheme) ? attrs.scrollTheme : 'dark-thin';
|
||||||
$(element).mCustomScrollbar({
|
$(element).mCustomScrollbar({
|
||||||
advanced:{
|
advanced:{
|
||||||
updateOnContentResize: true
|
updateOnContentResize: true
|
||||||
@@ -729,7 +730,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
|||||||
scrollButtons: {
|
scrollButtons: {
|
||||||
enable: true
|
enable: true
|
||||||
},
|
},
|
||||||
theme: 'dark-thin',
|
theme: theme,
|
||||||
mouseWheel: true,
|
mouseWheel: true,
|
||||||
scrollInertia: 300,
|
scrollInertia: 300,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "malihu-custom-scrollbar-plugin",
|
"name": "malihu-custom-scrollbar-plugin",
|
||||||
"version": "2.8.3",
|
"version": "2.8.4",
|
||||||
"title": "malihu custom scrollbar plugin",
|
"title": "malihu custom scrollbar plugin",
|
||||||
"description": "Custom scrollbar jQuery plugin that's fully customizable with CSS. Features vertical/horizontal scrolling, mouse-wheel support, scroll easing, adjustable scrollbar height/width, nested scrollbars, user defined callbacks etc.",
|
"description": "Custom scrollbar jQuery plugin that's fully customizable with CSS. Features vertical/horizontal scrolling, mouse-wheel support, scroll easing, adjustable scrollbar height/width, nested scrollbars, user defined callbacks etc.",
|
||||||
"main": "./jquery.mCustomScrollbar.js",
|
"main": "./jquery.mCustomScrollbar.js",
|
||||||
@@ -37,13 +37,13 @@
|
|||||||
"/source_files",
|
"/source_files",
|
||||||
"/js"
|
"/js"
|
||||||
],
|
],
|
||||||
"_release": "2.8.3",
|
"_release": "2.8.4",
|
||||||
"_resolution": {
|
"_resolution": {
|
||||||
"type": "version",
|
"type": "version",
|
||||||
"tag": "2.8.3",
|
"tag": "2.8.4",
|
||||||
"commit": "179ced37ef972b2c44bb4b71d20a6310fa317867"
|
"commit": "7f5be74468063f39e5f9bf0a8e2db380486c6e0e"
|
||||||
},
|
},
|
||||||
"_source": "git://github.com/malihu/malihu-custom-scrollbar-plugin.git",
|
"_source": "git://github.com/malihu/malihu-custom-scrollbar-plugin.git",
|
||||||
"_target": "2.8.3",
|
"_target": "2.8.4",
|
||||||
"_originalSource": "malihu-custom-scrollbar-plugin"
|
"_originalSource": "malihu-custom-scrollbar-plugin"
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
== malihu jquery custom scrollbars plugin ==
|
== malihu jquery custom scrollbars plugin ==
|
||||||
version: 2.8.3
|
version: 2.8.4
|
||||||
author: malihu (http://manos.malihu.gr)
|
author: malihu (http://manos.malihu.gr)
|
||||||
plugin home: http://manos.malihu.gr/jquery-custom-content-scroller
|
plugin home: http://manos.malihu.gr/jquery-custom-content-scroller
|
||||||
*/
|
*/
|
||||||
@@ -176,8 +176,9 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
percentage=maxHeight,
|
percentage=maxHeight,
|
||||||
maxHeight=$this.parent().height()*percentage/100;
|
maxHeight=$this.parent().height()*percentage/100;
|
||||||
}
|
}
|
||||||
|
var paddingY=($this.innerHeight()-$this.height());
|
||||||
$this.css("overflow","hidden");
|
$this.css("overflow","hidden");
|
||||||
mCustomScrollBox.css("max-height",maxHeight);
|
mCustomScrollBox.css("max-height",maxHeight-paddingY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this.mCustomScrollbar("update");
|
$this.mCustomScrollbar("update");
|
||||||
@@ -192,8 +193,12 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
if(!$this.is(".mCS_disabled") && !$this.is(".mCS_destroyed")){
|
if(!$this.is(".mCS_disabled") && !$this.is(".mCS_destroyed")){
|
||||||
var winWidth=$(window).width(),winHeight=$(window).height();
|
var winWidth=$(window).width(),winHeight=$(window).height();
|
||||||
if(currWinWidth!==winWidth || currWinHeight!==winHeight){ /*ie8 fix*/
|
if(currWinWidth!==winWidth || currWinHeight!==winHeight){ /*ie8 fix*/
|
||||||
if($this.css("max-height")!=="none" && percentage){
|
if($this.css("max-height")!=="none"){
|
||||||
mCustomScrollBox.css("max-height",$this.parent().height()*percentage/100);
|
if(percentage){
|
||||||
|
mCustomScrollBox.css("max-height",($this.parent().height()*percentage/100)-paddingY);
|
||||||
|
}else{
|
||||||
|
mCustomScrollBox.css("max-height",(parseInt($this.css("max-height"))-paddingY));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this.mCustomScrollbar("update");
|
$this.mCustomScrollbar("update");
|
||||||
currWinWidth=winWidth; currWinHeight=winHeight;
|
currWinWidth=winWidth; currWinHeight=winHeight;
|
||||||
@@ -206,22 +211,23 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
if(options.advanced.updateOnContentResize){
|
if(options.advanced.updateOnContentResize){
|
||||||
var mCSB_onContentResize;
|
var mCSB_onContentResize;
|
||||||
if(options.horizontalScroll){
|
if(options.horizontalScroll){
|
||||||
var mCSB_containerOldSize=mCSB_container.outerWidth();
|
var mCSB_containerOldSize=mCSB_container.outerWidth(),mCSB_contentOldSize=mCSB_container.innerWidth();
|
||||||
}else{
|
}else{
|
||||||
var mCSB_containerOldSize=mCSB_container.outerHeight();
|
var mCSB_containerOldSize=mCSB_container.outerHeight(),mCSB_contentOldSize=mCSB_container.innerHeight();
|
||||||
}
|
}
|
||||||
mCSB_onContentResize=setInterval(function(){
|
mCSB_onContentResize=setInterval(function(){
|
||||||
if(options.horizontalScroll){
|
if(options.horizontalScroll){
|
||||||
if(options.advanced.autoExpandHorizontalScroll){
|
if(options.advanced.autoExpandHorizontalScroll){
|
||||||
mCSB_container.css({"position":"absolute","width":"auto"}).wrap("<div class='mCSB_h_wrapper' style='position:relative; left:0; width:999999px;' />").css({"width":mCSB_container.outerWidth(),"position":"relative"}).unwrap();
|
mCSB_container.css({"position":"absolute","width":"auto"}).wrap("<div class='mCSB_h_wrapper' style='position:relative; left:0; width:999999px;' />").css({"width":mCSB_container.outerWidth(),"position":"relative"}).unwrap();
|
||||||
}
|
}
|
||||||
var mCSB_containerNewSize=mCSB_container.outerWidth();
|
var mCSB_containerNewSize=mCSB_container.outerWidth(),mCSB_contentNewSize=mCSB_container.innerWidth();
|
||||||
}else{
|
}else{
|
||||||
var mCSB_containerNewSize=mCSB_container.outerHeight();
|
var mCSB_containerNewSize=mCSB_container.outerHeight(),mCSB_contentNewSize=mCSB_container.innerHeight();
|
||||||
}
|
}
|
||||||
if(mCSB_containerNewSize!=mCSB_containerOldSize){
|
if(mCSB_containerNewSize!=mCSB_containerOldSize || mCSB_contentNewSize!=mCSB_contentOldSize){
|
||||||
$this.mCustomScrollbar("update");
|
$this.mCustomScrollbar("update");
|
||||||
mCSB_containerOldSize=mCSB_containerNewSize;
|
mCSB_containerOldSize=mCSB_containerNewSize;
|
||||||
|
mCSB_contentOldSize=mCSB_contentNewSize;
|
||||||
}
|
}
|
||||||
},300);
|
},300);
|
||||||
}
|
}
|
||||||
@@ -681,6 +687,18 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
}else{
|
}else{
|
||||||
draggerScrollTo=scrollTo=target;
|
draggerScrollTo=scrollTo=target;
|
||||||
}
|
}
|
||||||
|
}else if(typeof(scrollTo)==="object"){ /*if object, scroll by element position*/
|
||||||
|
var target=$(scrollTo);
|
||||||
|
if(target.length===1){ /*if such unique element exists, scroll to it*/
|
||||||
|
if($this.data("horizontalScroll")){
|
||||||
|
scrollTo=target.position().left;
|
||||||
|
}else{
|
||||||
|
scrollTo=target.position().top;
|
||||||
|
}
|
||||||
|
draggerScrollTo=scrollTo/$this.data("scrollAmount");
|
||||||
|
}else{
|
||||||
|
draggerScrollTo=scrollTo=target;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*scroll to*/
|
/*scroll to*/
|
||||||
if($this.data("horizontalScroll")){
|
if($this.data("horizontalScroll")){
|
||||||
@@ -767,7 +785,7 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
}
|
}
|
||||||
/*callbacks*/
|
/*callbacks*/
|
||||||
function callbacks(cb){
|
function callbacks(cb){
|
||||||
if ($this.data("mCustomScrollbarIndex")) {
|
if($this.data("mCustomScrollbarIndex")){
|
||||||
this.mcs = {
|
this.mcs = {
|
||||||
top: mCSB_container.position().top, left: mCSB_container.position().left,
|
top: mCSB_container.position().top, left: mCSB_container.position().left,
|
||||||
draggerTop: mCSB_dragger.position().top, draggerLeft: mCSB_dragger.position().left,
|
draggerTop: mCSB_dragger.position().top, draggerLeft: mCSB_dragger.position().left,
|
||||||
@@ -831,7 +849,9 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
functions={
|
functions={
|
||||||
/*hide/show scrollbar*/
|
/*hide/show scrollbar*/
|
||||||
showScrollbar:function(){
|
showScrollbar:function(){
|
||||||
this.stop().animate({opacity:1},"fast");
|
if($(this).css("opacity")==0){
|
||||||
|
this.stop().animate({opacity:1},"fast");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
hideScrollbar:function(){
|
hideScrollbar:function(){
|
||||||
this.stop().animate({opacity:0},"fast");
|
this.stop().animate({opacity:0},"fast");
|
||||||
@@ -955,7 +975,7 @@ along with this program. If not, see http://www.gnu.org/licenses/lgpl.html.
|
|||||||
$.support.msPointer=window.navigator.msPointerEnabled; /*MSPointer support*/
|
$.support.msPointer=window.navigator.msPointerEnabled; /*MSPointer support*/
|
||||||
/*plugin dependencies*/
|
/*plugin dependencies*/
|
||||||
var _dlp=("https:"==document.location.protocol) ? "https:" : "http:";
|
var _dlp=("https:"==document.location.protocol) ? "https:" : "http:";
|
||||||
$.event.special.mousewheel || document.write('<script src="'+_dlp+'//cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.0.6/jquery.mousewheel.min.js"><\/script>');
|
$.event.special.mousewheel || $("<script>",{src:_dlp+"//cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.6/jquery.mousewheel.min.js"}).appendTo("body");
|
||||||
/*plugin fn*/
|
/*plugin fn*/
|
||||||
$.fn.mCustomScrollbar=function(method){
|
$.fn.mCustomScrollbar=function(method){
|
||||||
if(methods[method]){
|
if(methods[method]){
|
||||||
|
|||||||
1
awx/ui/static/lib/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.min.js
vendored
Normal file
1
awx/ui/static/lib/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<iframe id="stdout-iframe"></iframe>
|
<div id="style-sheet-container"></div>
|
||||||
|
<div id="pre-container" class="body_background body_foreground pre mono-space" aw-custom-scroll data-scroll-theme="light-thin">
|
||||||
|
<div id="pre-container-content"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -402,7 +402,8 @@
|
|||||||
<script src="{{ STATIC_URL }}lib/codemirror/addon/selection/active-line.js"></script>
|
<script src="{{ STATIC_URL }}lib/codemirror/addon/selection/active-line.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/scrollto/lib/jquery-scrollto.js"></script>
|
<script src="{{ STATIC_URL }}lib/scrollto/lib/jquery-scrollto.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/socket.io-client/dist/socket.io.min.js"></script>
|
<script src="{{ STATIC_URL }}lib/socket.io-client/dist/socket.io.min.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js"></script>
|
<script src="{{ STATIC_URL }}lib/jquery-mousewheel/jquery.mousewheel.min.js"></script>
|
||||||
|
<script src="{{ STATIC_URL }}lib/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.min.js"></script>
|
||||||
<script scr="{{ STATIC_URL }}lib/lib/jQuery.dotdotdot/src/js/jquery.dotdotdot.min.js"></script>
|
<script scr="{{ STATIC_URL }}lib/lib/jQuery.dotdotdot/src/js/jquery.dotdotdot.min.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/d3js/build/d3.v3.min.js"></script>
|
<script src="{{ STATIC_URL }}lib/d3js/build/d3.v3.min.js"></script>
|
||||||
<script src="{{ STATIC_URL }}lib/d3Donut/d3Donut.js"></script>
|
<script src="{{ STATIC_URL }}lib/d3Donut/d3Donut.js"></script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"sizzle": "1.10.16",
|
"sizzle": "1.10.16",
|
||||||
"d3js": "*",
|
"d3js": "*",
|
||||||
"angular-tz-extensions": "~0.3.10",
|
"angular-tz-extensions": "~0.3.10",
|
||||||
"malihu-custom-scrollbar-plugin": "2.8.3",
|
"malihu-custom-scrollbar-plugin": "2.8.4",
|
||||||
"jQuery.dotdotdot": "~1.6.14"
|
"jQuery.dotdotdot": "~1.6.14"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
Reference in New Issue
Block a user