mirror of
https://github.com/pandorafms/pandorafms.git
synced 2025-07-31 01:35:36 +02:00
Merge branch 'ent-11093-reticula-en-consola-visual' into 'develop'
Ent 11093 reticula en consola visual See merge request artica/pandorafms!6056
This commit is contained in:
commit
7dea46c634
@ -1,5 +1,13 @@
|
|||||||
START TRANSACTION;
|
START TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE `tlayout`
|
||||||
|
ADD COLUMN `grid_color` VARCHAR(45) NOT NULL DEFAULT '#cccccc' AFTER `maintenance_mode`,
|
||||||
|
ADD COLUMN `grid_size` VARCHAR(45) NOT NULL DEFAULT '10' AFTER `grid_color`;
|
||||||
|
|
||||||
|
ALTER TABLE `tlayout_template`
|
||||||
|
ADD COLUMN `grid_color` VARCHAR(45) NOT NULL DEFAULT '#cccccc' AFTER `maintenance_mode`,
|
||||||
|
ADD COLUMN `grid_size` VARCHAR(45) NOT NULL DEFAULT '10' AFTER `grid_color`;
|
||||||
|
|
||||||
DELETE FROM tconfig WHERE token = 'refr';
|
DELETE FROM tconfig WHERE token = 'refr';
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
@ -39,6 +39,7 @@ $graph_javascript = (bool) get_parameter('graph_javascript', false);
|
|||||||
$force_remote_check = (bool) get_parameter('force_remote_check', false);
|
$force_remote_check = (bool) get_parameter('force_remote_check', false);
|
||||||
$update_maintanance_mode = (bool) get_parameter('update_maintanance_mode', false);
|
$update_maintanance_mode = (bool) get_parameter('update_maintanance_mode', false);
|
||||||
$load_css_cv = (bool) get_parameter('load_css_cv', false);
|
$load_css_cv = (bool) get_parameter('load_css_cv', false);
|
||||||
|
$update_grid_style = (bool) get_parameter('update_grid_style', false);
|
||||||
|
|
||||||
if ($render_map) {
|
if ($render_map) {
|
||||||
$width = (int) get_parameter('width', '400');
|
$width = (int) get_parameter('width', '400');
|
||||||
@ -126,3 +127,22 @@ if ($update_maintanance_mode === true) {
|
|||||||
echo json_encode(['result' => $result]);
|
echo json_encode(['result' => $result]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($update_grid_style === true) {
|
||||||
|
$idVisualConsole = (int) get_parameter('idVisualConsole', 0);
|
||||||
|
$color = get_parameter('color', '#CCC');
|
||||||
|
$size = get_parameter('size', '10');
|
||||||
|
|
||||||
|
$values = [];
|
||||||
|
$values['grid_color'] = $color;
|
||||||
|
$values['grid_size'] = $size;
|
||||||
|
|
||||||
|
$result = db_process_sql_update(
|
||||||
|
'tlayout',
|
||||||
|
$values,
|
||||||
|
['id' => $idVisualConsole]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode(['result' => $result]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@ -4423,7 +4423,7 @@ function html_print_checkbox_switch_extended(
|
|||||||
$name.($idcounter[$name] ? $idcounter[$name] : '')
|
$name.($idcounter[$name] ? $idcounter[$name] : '')
|
||||||
);
|
);
|
||||||
|
|
||||||
$output = '<label class="p-switch pdd_0px'.$classParent.'">';
|
$output = '<label class="p-switch pdd_0px '.$classParent.'">';
|
||||||
$output .= '<input name="'.$name.'" type="checkbox" value="'.$value.'" '.($checked ? 'checked="checked"' : '');
|
$output .= '<input name="'.$name.'" type="checkbox" value="'.$value.'" '.($checked ? 'checked="checked"' : '');
|
||||||
if ($id == '') {
|
if ($id == '') {
|
||||||
$output .= ' id="checkbox-'.$id_aux.'"';
|
$output .= ' id="checkbox-'.$id_aux.'"';
|
||||||
|
@ -252,6 +252,20 @@ function createVisualConsole(
|
|||||||
});
|
});
|
||||||
// VC Item moved.
|
// VC Item moved.
|
||||||
visualConsole.onItemMoved(function(e) {
|
visualConsole.onItemMoved(function(e) {
|
||||||
|
if (
|
||||||
|
$("input[name=grid-mode]").prop("checked") &&
|
||||||
|
e.item.props.type !== 13 &&
|
||||||
|
e.item.props.type !== 21
|
||||||
|
) {
|
||||||
|
var gridSize = $("#grid_size").val();
|
||||||
|
var positionX = e.newPosition.x;
|
||||||
|
var positionY = e.newPosition.y;
|
||||||
|
if (positionX % gridSize !== 0 || positionY % gridSize !== 0) {
|
||||||
|
e.newPosition.x = Math.floor(positionX / gridSize) * gridSize;
|
||||||
|
e.newPosition.y = Math.floor(positionY / gridSize) * gridSize;
|
||||||
|
e.item.move(e.newPosition.x, e.newPosition.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
var id = e.item.props.id;
|
var id = e.item.props.id;
|
||||||
var data = {
|
var data = {
|
||||||
x: e.newPosition.x,
|
x: e.newPosition.x,
|
||||||
|
@ -91,6 +91,8 @@ final class Container extends Model
|
|||||||
'relationLineWidth' => (int) $data['relationLineWidth'],
|
'relationLineWidth' => (int) $data['relationLineWidth'],
|
||||||
'hash' => static::extractHash($data),
|
'hash' => static::extractHash($data),
|
||||||
'maintenanceMode' => static::extractMaintenanceMode($data),
|
'maintenanceMode' => static::extractMaintenanceMode($data),
|
||||||
|
'gridSize' => (int) $data['grid_size'],
|
||||||
|
'gridColor' => (string) $data['grid_color'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6923,6 +6923,10 @@ div.graph div.legend table {
|
|||||||
/*
|
/*
|
||||||
* MARGIN LEFT
|
* MARGIN LEFT
|
||||||
*/
|
*/
|
||||||
|
.mrgn_lft-20px {
|
||||||
|
margin-left: -20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mrgn_lft_05em {
|
.mrgn_lft_05em {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
@ -12261,6 +12265,11 @@ div.parent_graph > p.legend_background > table > tbody > tr {
|
|||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#grid_img {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
div#visual-console-container * {
|
div#visual-console-container * {
|
||||||
font-size: unset;
|
font-size: unset;
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -463,6 +463,68 @@ if ($pure === false) {
|
|||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo '<div id ="grid-controls" class="flex-colum-center center_switch" style="visibility:hidden">';
|
||||||
|
echo html_print_label(__('Grid'), 'grid-mode', true);
|
||||||
|
echo '<div>';
|
||||||
|
echo html_print_checkbox_switch_extended('grid-mode', 1, false, $disabled_edit_mode, '', '', true, '', 'mrgn_lft-20px');
|
||||||
|
echo html_print_image(
|
||||||
|
'images/configuration@svg.svg',
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
'title' => __('Grid style'),
|
||||||
|
'class' => 'main_menu_icon invert_filter invisible',
|
||||||
|
'style' => 'position: absolute; margin-left: 5px;',
|
||||||
|
'id' => 'grid_img',
|
||||||
|
'onclick' => 'dialog_grid()',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
echo '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '<div id="dialog_grid" class="invisible">';
|
||||||
|
$table = new stdClass();
|
||||||
|
$table->width = '100%';
|
||||||
|
$table->class = 'filter-table-adv';
|
||||||
|
$table->size[0] = '50%';
|
||||||
|
$table->size[1] = '50%';
|
||||||
|
$table->data = [];
|
||||||
|
$table->data[0][0] = html_print_label_input_block(
|
||||||
|
__('Grid size'),
|
||||||
|
html_print_input_number(
|
||||||
|
[
|
||||||
|
'name' => 'grid_size',
|
||||||
|
'value' => $visualConsoleData['gridSize'],
|
||||||
|
'id' => 'grid_size',
|
||||||
|
'min' => 2,
|
||||||
|
'max' => 50,
|
||||||
|
'return' => true,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$table->data[0][1] = html_print_label_input_block(
|
||||||
|
__('Grid color'),
|
||||||
|
html_print_input_color(
|
||||||
|
'grid_color',
|
||||||
|
$visualConsoleData['gridColor'],
|
||||||
|
'grid_color',
|
||||||
|
'w100p',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
html_print_table($table);
|
||||||
|
html_print_submit_button(
|
||||||
|
__('Update'),
|
||||||
|
'grid_setup',
|
||||||
|
false,
|
||||||
|
[
|
||||||
|
'icon' => 'next',
|
||||||
|
'class' => 'float-right',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
echo '<div id="edit-mode-control" class="flex-colum-center center_switch">';
|
echo '<div id="edit-mode-control" class="flex-colum-center center_switch">';
|
||||||
echo html_print_label(__('Edit'), 'edit-mode', true);
|
echo html_print_label(__('Edit'), 'edit-mode', true);
|
||||||
echo html_print_checkbox_switch('edit-mode', 1, false, true, $disabled_edit_mode);
|
echo html_print_checkbox_switch('edit-mode', 1, false, true, $disabled_edit_mode);
|
||||||
@ -768,16 +830,106 @@ if ($edit_capable === true) {
|
|||||||
visualConsoleManager.visualConsole.enableEditMode();
|
visualConsoleManager.visualConsole.enableEditMode();
|
||||||
visualConsoleManager.changeUpdateInterval(0);
|
visualConsoleManager.changeUpdateInterval(0);
|
||||||
$('#edit-controls').css('visibility', '');
|
$('#edit-controls').css('visibility', '');
|
||||||
|
$('#grid-controls').css('visibility', '');
|
||||||
} else {
|
} else {
|
||||||
visualConsoleManager.visualConsole.disableEditMode();
|
visualConsoleManager.visualConsole.disableEditMode();
|
||||||
visualConsoleManager.visualConsole.unSelectItems();
|
visualConsoleManager.visualConsole.unSelectItems();
|
||||||
visualConsoleManager.changeUpdateInterval(<?php echo ($refr * 1000); ?>); // To ms.
|
visualConsoleManager.changeUpdateInterval(<?php echo ($refr * 1000); ?>); // To ms.
|
||||||
$('#edit-controls').css('visibility', 'hidden');
|
$('#edit-controls').css('visibility', 'hidden');
|
||||||
|
$('#grid-controls').css('visibility', 'hidden');
|
||||||
|
$('input[name=grid-mode]').prop('checked', false);
|
||||||
|
$('#div-grid').remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetInterval();
|
resetInterval();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('input[name=grid-mode]').change(function(evente) {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
color = $('#grid_color').val();
|
||||||
|
size = $('#grid_size').val();
|
||||||
|
display_grid(color,size);
|
||||||
|
$('#grid_img').removeClass('invisible');
|
||||||
|
visualConsoleManager.visualConsole.updateGridSelected(true);
|
||||||
|
} else {
|
||||||
|
$('#div-grid').remove();
|
||||||
|
$('#grid_img').addClass('invisible');
|
||||||
|
visualConsoleManager.visualConsole.updateGridSelected(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#button-grid_setup').click(function(){
|
||||||
|
if(validate_size()){
|
||||||
|
color = $('#grid_color').val();
|
||||||
|
size = $('#grid_size').val();
|
||||||
|
display_grid(color,size);
|
||||||
|
$('#dialog_grid').dialog('close');
|
||||||
|
save_grid_style(color, size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#grid_size').blur(function(){
|
||||||
|
validate_size();
|
||||||
|
});
|
||||||
|
|
||||||
|
function validate_size(){
|
||||||
|
if($('#grid_size').val()<2 || $('#grid_size').val()>50){
|
||||||
|
$('#grid_size').val('10');
|
||||||
|
alert("<?php echo __('The size should be between 2 and 50'); ?>");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialog_grid(){
|
||||||
|
$('#dialog_grid').dialog({
|
||||||
|
title: '<?php echo __('Grid style'); ?>',
|
||||||
|
resizable: true,
|
||||||
|
draggable: true,
|
||||||
|
modal: true,
|
||||||
|
close: false,
|
||||||
|
height: 200,
|
||||||
|
width: 480,
|
||||||
|
overlay: {
|
||||||
|
opacity: 0.5,
|
||||||
|
background: "black"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_grid(color='#ccc', size='10'){
|
||||||
|
$('#div-grid').remove();
|
||||||
|
var grid = "<div id='div-grid' style='background-image: linear-gradient("+color+" .1em, transparent .1em), linear-gradient(90deg, "+color+", .1em, transparent .1em); background-size: "+size+"px "+size+"px;height: 100%;width: 100%;'></div>";
|
||||||
|
$('#visual-console-container').append(grid);
|
||||||
|
};
|
||||||
|
|
||||||
|
function save_grid_style(color, size){
|
||||||
|
const idVisualConsole = '<?php echo $visualConsoleId; ?>';
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "ajax.php",
|
||||||
|
dataType: "json",
|
||||||
|
data: {
|
||||||
|
page: "include/ajax/visual_console.ajax",
|
||||||
|
update_grid_style: true,
|
||||||
|
color: color,
|
||||||
|
size: size,
|
||||||
|
idVisualConsole: idVisualConsole,
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
if(data.result) {
|
||||||
|
alert("<?php echo __('Grid style saved.'); ?>");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
visualConsoleManager.visualConsole.updateGridSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
// Enable/disable the maintenance mode.
|
// Enable/disable the maintenance mode.
|
||||||
$('input[name=maintenance-mode]').click(function(event) {
|
$('input[name=maintenance-mode]').click(function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -1700,6 +1700,8 @@ CREATE TABLE IF NOT EXISTS `tlayout` (
|
|||||||
`is_favourite` INT UNSIGNED NOT NULL DEFAULT 0,
|
`is_favourite` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`auto_adjust` INT UNSIGNED NOT NULL DEFAULT 0,
|
`auto_adjust` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`maintenance_mode` TEXT,
|
`maintenance_mode` TEXT,
|
||||||
|
`grid_color` VARCHAR(45) NOT NULL DEFAULT '#cccccc',
|
||||||
|
`grid_size` VARCHAR(45) NOT NULL DEFAULT '10',
|
||||||
PRIMARY KEY(`id`)
|
PRIMARY KEY(`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;
|
) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;
|
||||||
|
|
||||||
@ -3666,6 +3668,8 @@ CREATE TABLE IF NOT EXISTS `tlayout_template` (
|
|||||||
`is_favourite` INT UNSIGNED NOT NULL DEFAULT 0,
|
`is_favourite` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`auto_adjust` INT UNSIGNED NOT NULL DEFAULT 0,
|
`auto_adjust` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`maintenance_mode` TEXT,
|
`maintenance_mode` TEXT,
|
||||||
|
`grid_color` VARCHAR(45) NOT NULL DEFAULT '#cccccc',
|
||||||
|
`grid_size` VARCHAR(45) NOT NULL DEFAULT '10',
|
||||||
PRIMARY KEY(`id`)
|
PRIMARY KEY(`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;
|
) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ export function titleItem(id: number): string {
|
|||||||
*/
|
*/
|
||||||
abstract class VisualConsoleItem<Props extends ItemProps> {
|
abstract class VisualConsoleItem<Props extends ItemProps> {
|
||||||
// Properties of the item.
|
// Properties of the item.
|
||||||
private itemProps: Props;
|
public itemProps: Props;
|
||||||
// Metadata of the item.
|
// Metadata of the item.
|
||||||
private _metadata: ItemMeta;
|
private _metadata: ItemMeta;
|
||||||
// Reference to the DOM element which will contain the item.
|
// Reference to the DOM element which will contain the item.
|
||||||
@ -955,8 +955,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
|
|||||||
if (!prevMeta || prevMeta.isSelected !== this.meta.isSelected) {
|
if (!prevMeta || prevMeta.isSelected !== this.meta.isSelected) {
|
||||||
if (this.meta.isSelected) {
|
if (this.meta.isSelected) {
|
||||||
this.elementRef.classList.add("is-selected");
|
this.elementRef.classList.add("is-selected");
|
||||||
|
this.elementRef.setAttribute("id", "item-selected-move");
|
||||||
} else {
|
} else {
|
||||||
this.elementRef.classList.remove("is-selected");
|
this.elementRef.classList.remove("is-selected");
|
||||||
|
this.elementRef.removeAttribute("id");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,6 +160,8 @@ export interface VisualConsoleProps extends Size {
|
|||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
relationLineWidth: number;
|
relationLineWidth: number;
|
||||||
maintenanceMode: MaintenanceModeInterface | null;
|
maintenanceMode: MaintenanceModeInterface | null;
|
||||||
|
gridSize: number | 10;
|
||||||
|
gridSelected: boolean | false | false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaintenanceModeInterface {
|
export interface MaintenanceModeInterface {
|
||||||
@ -188,7 +190,9 @@ export function visualConsolePropsDecoder(
|
|||||||
backgroundColor,
|
backgroundColor,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
relationLineWidth,
|
relationLineWidth,
|
||||||
maintenanceMode
|
maintenanceMode,
|
||||||
|
gridSize,
|
||||||
|
gridSelected
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
if (id == null || isNaN(parseInt(id))) {
|
if (id == null || isNaN(parseInt(id))) {
|
||||||
@ -210,6 +214,8 @@ export function visualConsolePropsDecoder(
|
|||||||
isFavorite: parseBoolean(isFavorite),
|
isFavorite: parseBoolean(isFavorite),
|
||||||
relationLineWidth: parseIntOr(relationLineWidth, 0),
|
relationLineWidth: parseIntOr(relationLineWidth, 0),
|
||||||
maintenanceMode: maintenanceMode,
|
maintenanceMode: maintenanceMode,
|
||||||
|
gridSize: parseIntOr(gridSize, 10),
|
||||||
|
gridSelected: false,
|
||||||
...sizePropsDecoder(data)
|
...sizePropsDecoder(data)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -279,6 +285,24 @@ export default class VisualConsole {
|
|||||||
* @param e Event object.
|
* @param e Event object.
|
||||||
*/
|
*/
|
||||||
private handleElementMovement: (e: ItemMovedEvent) => void = e => {
|
private handleElementMovement: (e: ItemMovedEvent) => void = e => {
|
||||||
|
var type = e.item.itemProps.type;
|
||||||
|
if (type !== 13 && type !== 21 && this.props.gridSelected === true) {
|
||||||
|
var gridSize = this.props.gridSize;
|
||||||
|
var positionX = e.newPosition.x;
|
||||||
|
var positionY = e.newPosition.y;
|
||||||
|
if (positionX % gridSize !== 0 || positionY % gridSize !== 0) {
|
||||||
|
var x = Math.floor(positionX / gridSize) * gridSize;
|
||||||
|
var y = Math.floor(positionY / gridSize) * gridSize;
|
||||||
|
let elemntSelected = document.getElementById(
|
||||||
|
"item-selected-move"
|
||||||
|
) as HTMLElement;
|
||||||
|
elemntSelected.setAttribute(
|
||||||
|
"style",
|
||||||
|
"top:" + y + "px !important; left:" + x + "px !important"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Move their relation lines.
|
// Move their relation lines.
|
||||||
const itemId = e.item.props.id;
|
const itemId = e.item.props.id;
|
||||||
const relations = this.getItemRelations(itemId);
|
const relations = this.getItemRelations(itemId);
|
||||||
@ -1269,6 +1293,22 @@ export default class VisualConsole {
|
|||||||
this.containerRef.classList.add("is-editing");
|
this.containerRef.classList.add("is-editing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the gridSize.
|
||||||
|
*/
|
||||||
|
public updateGridSize(gridSize: string): void {
|
||||||
|
this._props.gridSize = parseInt(gridSize);
|
||||||
|
this.props.gridSize = parseInt(gridSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the gridSize.
|
||||||
|
*/
|
||||||
|
public updateGridSelected(gridSelected: boolean): void {
|
||||||
|
this._props.gridSelected = gridSelected;
|
||||||
|
this.props.gridSelected = gridSelected;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select an item.
|
* Select an item.
|
||||||
* @param itemId Item Id.
|
* @param itemId Item Id.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user