[WIP] directory watcher
This commit is contained in:
parent
d683170a73
commit
f7d9aff838
@ -59,7 +59,28 @@ runtime {
|
|||||||
)
|
)
|
||||||
jpackage {
|
jpackage {
|
||||||
jvmArgs = addJvmArgs
|
jvmArgs = addJvmArgs
|
||||||
//imageOptions = listOf("--linux-deb-maintainer", "nozik.aa@mipt.ru", "--linux-menu-group", "Science")
|
val currentOs = org.gradle.internal.os.OperatingSystem.current()
|
||||||
|
installerOptions = installerOptions + listOf("--vendor", "MIPT-NPM lab")
|
||||||
|
|
||||||
|
if (currentOs.isWindows) {
|
||||||
|
installerOptions = installerOptions + listOf(
|
||||||
|
"--win-menu",
|
||||||
|
"--win-menu-group", "Numass",
|
||||||
|
"--win-dir-chooser",
|
||||||
|
"--win-shortcut"
|
||||||
|
)
|
||||||
|
} else if (currentOs.isLinux) {
|
||||||
|
installerType = "deb"
|
||||||
|
installerOptions = installerOptions + listOf(
|
||||||
|
"--linux-package-name", "numass-viewer",
|
||||||
|
"--linux-shortcut"
|
||||||
|
)
|
||||||
|
imageOptions = listOf(
|
||||||
|
"--linux-deb-maintainer", "nozik.aa@mipt.ru",
|
||||||
|
"--linux-menu-group", "Science",
|
||||||
|
"--linux-shortcut"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
launcher {
|
launcher {
|
||||||
jvmArgs = addJvmArgs
|
jvmArgs = addJvmArgs
|
||||||
|
@ -56,7 +56,7 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
|||||||
var normalize by normalizeProperty
|
var normalize by normalizeProperty
|
||||||
|
|
||||||
|
|
||||||
private val container = PlotContainer(frame).apply {
|
private val plotContainer = PlotContainer(frame).apply {
|
||||||
val binningSelector: ChoiceBox<Int> = ChoiceBox(FXCollections.observableArrayList(1, 2, 8, 16, 32, 50)).apply {
|
val binningSelector: ChoiceBox<Int> = ChoiceBox(FXCollections.observableArrayList(1, 2, 8, 16, 32, 50)).apply {
|
||||||
minWidth = 0.0
|
minWidth = 0.0
|
||||||
selectionModel.selectLast()
|
selectionModel.selectLast()
|
||||||
@ -71,7 +71,6 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
|||||||
|
|
||||||
private val plotJobs: ObservableMap<String, Job> = FXCollections.observableHashMap()
|
private val plotJobs: ObservableMap<String, Job> = FXCollections.observableHashMap()
|
||||||
|
|
||||||
val isEmpty = booleanBinding(data) { isEmpty() }
|
|
||||||
|
|
||||||
private val progress = object : DoubleBinding() {
|
private val progress = object : DoubleBinding() {
|
||||||
init {
|
init {
|
||||||
@ -102,7 +101,7 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
|||||||
replot()
|
replot()
|
||||||
}
|
}
|
||||||
|
|
||||||
container.progressProperty.bind(progress)
|
plotContainer.progressProperty.bind(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replotOne(key: String, point: DataController.CachedPoint) {
|
private fun replotOne(key: String, point: DataController.CachedPoint) {
|
||||||
@ -159,6 +158,6 @@ class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = Imag
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val root = borderpane {
|
override val root = borderpane {
|
||||||
center = container.root
|
center = plotContainer.root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ class DataController : Controller() {
|
|||||||
|
|
||||||
val voltage = point.voltage
|
val voltage = point.voltage
|
||||||
|
|
||||||
|
val index = point.index
|
||||||
|
|
||||||
val meta = point.meta
|
val meta = point.meta
|
||||||
|
|
||||||
val channelSpectra: Deferred<Map<Int, Table>> = context.async(Dispatchers.IO) {
|
val channelSpectra: Deferred<Map<Int, Table>> = context.async(Dispatchers.IO) {
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
package inr.numass.viewer
|
||||||
|
|
||||||
|
import hep.dataforge.fx.dfIconView
|
||||||
|
import hep.dataforge.io.envelopes.Envelope
|
||||||
|
import inr.numass.data.NumassDataUtils
|
||||||
|
import inr.numass.data.NumassEnvelopeType
|
||||||
|
import inr.numass.data.storage.NumassDataLoader
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.MapChangeListener
|
||||||
|
import javafx.scene.control.ContextMenu
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import tornadofx.*
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
||||||
|
import java.nio.file.WatchKey
|
||||||
|
|
||||||
|
class DirectoryWatchView : View(title = "Numass storage", icon = dfIconView) {
|
||||||
|
|
||||||
|
val pathProperty = SimpleObjectProperty<Path>()
|
||||||
|
private val dataController by inject<DataController>()
|
||||||
|
|
||||||
|
private val ampView: AmplitudeView by inject()
|
||||||
|
private val timeView: TimeView by inject()
|
||||||
|
|
||||||
|
private var watcherProperty = pathProperty.objectBinding {
|
||||||
|
it?.fileSystem?.newWatchService()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val files = FXCollections.observableArrayList<DataController.CachedPoint>()
|
||||||
|
|
||||||
|
private var watchJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataController.points.addListener(MapChangeListener { change ->
|
||||||
|
if (change.wasAdded()) {
|
||||||
|
files.add(change.valueAdded)
|
||||||
|
} else if (change.wasRemoved()) {
|
||||||
|
files.remove(change.valueRemoved)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watcherProperty.onChange { watchService ->
|
||||||
|
watchJob?.cancel()
|
||||||
|
if (watchService != null) {
|
||||||
|
watchJob = app.context.launch(Dispatchers.IO) {
|
||||||
|
val key: WatchKey = pathProperty.get().register(watchService, ENTRY_CREATE)
|
||||||
|
coroutineContext[Job]?.invokeOnCompletion {
|
||||||
|
key.cancel()
|
||||||
|
}
|
||||||
|
while (isActive) {
|
||||||
|
try {
|
||||||
|
key.pollEvents().forEach { event ->
|
||||||
|
if (event.kind() == ENTRY_CREATE) {
|
||||||
|
val path: Path = event.context() as Path
|
||||||
|
if (path.fileName.toString().startsWith(NumassDataLoader.POINT_FRAGMENT_NAME)) {
|
||||||
|
val envelope: Envelope = NumassEnvelopeType.infer(path)?.reader?.read(path)
|
||||||
|
?: kotlin.error("Can't read point file")
|
||||||
|
val point = NumassDataUtils.read(envelope)
|
||||||
|
files.add(dataController.getCachedPoint(path.toString(), point))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (x: Throwable) {
|
||||||
|
app.context.logger.error("Error during dynamic point read", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override val root = splitpane {
|
||||||
|
listview(files) {
|
||||||
|
multiSelect(true)
|
||||||
|
cellFormat { value: DataController.CachedPoint ->
|
||||||
|
text = "${value.voltage}[${value.index}]"
|
||||||
|
graphic = null
|
||||||
|
contextMenu = ContextMenu().apply {
|
||||||
|
item("Info") {
|
||||||
|
action {
|
||||||
|
PointInfoView(value).openModal(escapeClosesWindow = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tabpane {
|
||||||
|
tab("Amplitude spectra") {
|
||||||
|
content = ampView.root
|
||||||
|
isClosable = false
|
||||||
|
//visibleWhen(ampView.isEmpty.not())
|
||||||
|
}
|
||||||
|
tab("Time spectra") {
|
||||||
|
content = timeView.root
|
||||||
|
isClosable = false
|
||||||
|
//visibleWhen(ampView.isEmpty.not())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDividerPosition(0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,17 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
|||||||
|
|
||||||
private val contentViewProperty = SimpleObjectProperty<UIComponent>()
|
private val contentViewProperty = SimpleObjectProperty<UIComponent>()
|
||||||
private var contentView: UIComponent? by contentViewProperty
|
private var contentView: UIComponent? by contentViewProperty
|
||||||
|
private val spectrumView by inject<SpectrumView>()
|
||||||
|
private val amplitudeView by inject<AmplitudeView>()
|
||||||
|
private val directoryWatchView by inject<DirectoryWatchView>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
contentViewProperty.onChange {
|
||||||
|
root.center = it?.root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override val root = borderpane {
|
override val root = borderpane {
|
||||||
prefHeight = 600.0
|
prefHeight = 600.0
|
||||||
@ -72,79 +83,11 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
|||||||
if (rootDir != null) {
|
if (rootDir != null) {
|
||||||
NumassProperties.setNumassProperty("numass.viewer.lastPath", rootDir.absolutePath)
|
NumassProperties.setNumassProperty("numass.viewer.lastPath", rootDir.absolutePath)
|
||||||
app.context.launch {
|
app.context.launch {
|
||||||
|
dataController.clear()
|
||||||
runLater {
|
runLater {
|
||||||
path = rootDir.toPath()
|
path = rootDir.toPath()
|
||||||
}
|
|
||||||
load(rootDir.toPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
NumassProperties.setNumassProperty("numass.viewer.lastPath", null)
|
|
||||||
error("Error", content = "Failed to laod file with message: ${ex.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button("Load file") {
|
|
||||||
action {
|
|
||||||
val chooser = FileChooser()
|
|
||||||
chooser.title = "Select file to view"
|
|
||||||
val homeDir = NumassProperties.getNumassProperty("numass.viewer.lastPath")
|
|
||||||
try {
|
|
||||||
if (homeDir == null) {
|
|
||||||
chooser.initialDirectory = File(".").absoluteFile
|
|
||||||
} else {
|
|
||||||
chooser.initialDirectory = File(homeDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val file = chooser.showOpenDialog(primaryStage.scene.window)
|
|
||||||
if (file != null) {
|
|
||||||
NumassProperties.setNumassProperty("numass.viewer.lastPath",
|
|
||||||
file.parentFile.absolutePath)
|
|
||||||
app.context.launch {
|
|
||||||
runLater {
|
|
||||||
path = file.toPath()
|
|
||||||
}
|
|
||||||
load(file.toPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
NumassProperties.setNumassProperty("numass.viewer.lastPath", null)
|
|
||||||
error("Error", content = "Failed to laod file with message: ${ex.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label(pathProperty.stringBinding { it?.toString() ?: "NOT LOADED" }) {
|
|
||||||
padding = Insets(0.0, 0.0, 0.0, 10.0)
|
|
||||||
font = Font.font("System Bold", 13.0)
|
|
||||||
}
|
|
||||||
pane {
|
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
}
|
|
||||||
// togglebutton("Console") {
|
|
||||||
// isSelected = false
|
|
||||||
// logFragment.bindWindow(this@togglebutton)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bottom = statusBar
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
contentViewProperty.onChange {
|
|
||||||
root.center = it?.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val spectrumView by inject<SpectrumView>()
|
|
||||||
|
|
||||||
private suspend fun load(path: Path) {
|
|
||||||
runLater {
|
|
||||||
contentView = null
|
contentView = null
|
||||||
}
|
}
|
||||||
dataController.clear()
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
if (Files.exists(path.resolve(NumassDataLoader.META_FRAGMENT_NAME))) {
|
if (Files.exists(path.resolve(NumassDataLoader.META_FRAGMENT_NAME))) {
|
||||||
//build set view
|
//build set view
|
||||||
runGoal(app.context, "viewer.load.set[$path]", Dispatchers.IO) {
|
runGoal(app.context, "viewer.load.set[$path]", Dispatchers.IO) {
|
||||||
@ -172,7 +115,37 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
NumassProperties.setNumassProperty("numass.viewer.lastPath", null)
|
||||||
|
error("Error", content = "Failed to laod file with message: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button("Load file") {
|
||||||
|
action {
|
||||||
|
val chooser = FileChooser()
|
||||||
|
chooser.title = "Select file to view"
|
||||||
|
val homeDir = NumassProperties.getNumassProperty("numass.viewer.lastPath")
|
||||||
|
try {
|
||||||
|
if (homeDir == null) {
|
||||||
|
chooser.initialDirectory = File(".").absoluteFile
|
||||||
} else {
|
} else {
|
||||||
|
chooser.initialDirectory = File(homeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val file = chooser.showOpenDialog(primaryStage.scene.window)
|
||||||
|
if (file != null) {
|
||||||
|
NumassProperties.setNumassProperty("numass.viewer.lastPath",
|
||||||
|
file.parentFile.absolutePath)
|
||||||
|
app.context.launch {
|
||||||
|
dataController.clear()
|
||||||
|
runLater {
|
||||||
|
path = file.toPath()
|
||||||
|
contentView = null
|
||||||
|
}
|
||||||
//Reading individual file
|
//Reading individual file
|
||||||
val envelope = try {
|
val envelope = try {
|
||||||
NumassFileEnvelope(path)
|
NumassFileEnvelope(path)
|
||||||
@ -191,12 +164,69 @@ class MainView : View(title = "Numass viewer", icon = dfIconView) {
|
|||||||
//try to read as point
|
//try to read as point
|
||||||
val point = NumassDataUtils.read(it)
|
val point = NumassDataUtils.read(it)
|
||||||
runLater {
|
runLater {
|
||||||
contentView = AmplitudeView().apply {
|
contentView = amplitudeView
|
||||||
dataController.addPoint(path.fileName.toString(), point)
|
dataController.addPoint(path.fileName.toString(), point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
NumassProperties.setNumassProperty("numass.viewer.lastPath", null)
|
||||||
|
error("Error", content = "Failed to laod file with message: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button("Watch directory") {
|
||||||
|
action {
|
||||||
|
val chooser = DirectoryChooser()
|
||||||
|
chooser.title = "Select directory to view"
|
||||||
|
val homeDir = NumassProperties.getNumassProperty("numass.viewer.lastPath")
|
||||||
|
try {
|
||||||
|
if (homeDir == null) {
|
||||||
|
chooser.initialDirectory = File(".").absoluteFile
|
||||||
|
} else {
|
||||||
|
val file = File(homeDir)
|
||||||
|
if (file.isDirectory) {
|
||||||
|
chooser.initialDirectory = file
|
||||||
|
} else {
|
||||||
|
chooser.initialDirectory = file.parentFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dir = chooser.showDialog(primaryStage.scene.window)
|
||||||
|
|
||||||
|
if (dir != null) {
|
||||||
|
NumassProperties.setNumassProperty("numass.viewer.lastPath", dir.absolutePath)
|
||||||
|
app.context.launch {
|
||||||
|
dataController.clear()
|
||||||
|
runLater {
|
||||||
|
path = dir.toPath()
|
||||||
|
contentView = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
NumassProperties.setNumassProperty("numass.viewer.lastPath", null)
|
||||||
|
error("Error", content = "Failed to laod file with message: ${ex.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label(pathProperty.stringBinding { it?.toString() ?: "NOT LOADED" }) {
|
||||||
|
padding = Insets(0.0, 0.0, 0.0, 10.0)
|
||||||
|
font = Font.font("System Bold", 13.0)
|
||||||
|
}
|
||||||
|
pane {
|
||||||
|
hgrow = Priority.ALWAYS
|
||||||
|
}
|
||||||
|
// togglebutton("Console") {
|
||||||
|
// isSelected = false
|
||||||
|
// logFragment.bindWindow(this@togglebutton)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bottom = statusBar
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,6 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
|||||||
private val hvView: HVView by inject()
|
private val hvView: HVView by inject()
|
||||||
private val scView: SlowControlView by inject()
|
private val scView: SlowControlView by inject()
|
||||||
|
|
||||||
// private var watcher: WatchService? = null
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
dataController.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class Container(val id: String, val content: Any) {
|
private inner class Container(val id: String, val content: Any) {
|
||||||
val checkedProperty = SimpleBooleanProperty(false)
|
val checkedProperty = SimpleBooleanProperty(false)
|
||||||
var checked by checkedProperty
|
var checked by checkedProperty
|
||||||
@ -102,38 +96,6 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
|||||||
|
|
||||||
private var watchJob: Job? = null
|
private var watchJob: Job? = null
|
||||||
|
|
||||||
// private fun toggleWatch(watch: Boolean) {
|
|
||||||
// if (watch) {
|
|
||||||
// if (watchJob != null && content is NumassDataLoader) {
|
|
||||||
// watchJob = app.context.launch(Dispatchers.IO) {
|
|
||||||
// val key: WatchKey = content.path.register(watcher!!, ENTRY_CREATE)
|
|
||||||
// coroutineContext[Job]?.invokeOnCompletion {
|
|
||||||
// key.cancel()
|
|
||||||
// }
|
|
||||||
// while (watcher != null && isActive) {
|
|
||||||
// try {
|
|
||||||
// key.pollEvents().forEach { event ->
|
|
||||||
// if (event.kind() == ENTRY_CREATE) {
|
|
||||||
// val path: Path = event.context() as Path
|
|
||||||
// if (path.fileName.toString().startsWith(NumassDataLoader.POINT_FRAGMENT_NAME)) {
|
|
||||||
// val envelope: Envelope = NumassEnvelopeType.infer(path)?.reader?.read(path)
|
|
||||||
// ?: kotlin.error("Can't read point file")
|
|
||||||
// val point = NumassDataUtils.read(envelope)
|
|
||||||
// children!!.add(buildContainer(point, this@Container))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (x: Throwable) {
|
|
||||||
// app.context.logger.error("Error during dynamic point read", x)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// watchJob?.cancel()
|
|
||||||
// watchJob = null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -141,7 +103,7 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
|||||||
treeview<Container> {
|
treeview<Container> {
|
||||||
//isShowRoot = false
|
//isShowRoot = false
|
||||||
storageProperty.onChange { storage ->
|
storageProperty.onChange { storage ->
|
||||||
clear()
|
dataController.clear()
|
||||||
if (storage == null) return@onChange
|
if (storage == null) return@onChange
|
||||||
root = TreeItem(Container(storage.name, storage))
|
root = TreeItem(Container(storage.name, storage))
|
||||||
root.isExpanded = true
|
root.isExpanded = true
|
||||||
@ -150,12 +112,7 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
|||||||
}) {
|
}) {
|
||||||
it.value.children
|
it.value.children
|
||||||
}
|
}
|
||||||
// watcher?.close()
|
|
||||||
// watcher = if (storage is FileStorage) {
|
|
||||||
// storage.path.fileSystem.newWatchService()
|
|
||||||
// } else {
|
|
||||||
// null
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cellFormat { value: Container ->
|
cellFormat { value: Container ->
|
||||||
@ -198,11 +155,6 @@ class StorageView : View(title = "Numass storage", icon = dfIconView) {
|
|||||||
value.infoView.openModal(escapeClosesWindow = true)
|
value.infoView.openModal(escapeClosesWindow = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if(value.content is NumassDataLoader) {
|
|
||||||
// checkmenuitem("Watch") {
|
|
||||||
// selectedProperty().bindBidirectional(value.watchedProperty)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user