2017-01-16 20:59:50 +01:00
|
|
|
|
/* Copyright (c) Citrix Systems, Inc.
|
2013-06-24 13:41:48 +02:00
|
|
|
|
* All rights reserved.
|
|
|
|
|
*
|
|
|
|
|
* Redistribution and use in source and binary forms,
|
|
|
|
|
* with or without modification, are permitted provided
|
|
|
|
|
* that the following conditions are met:
|
|
|
|
|
*
|
|
|
|
|
* * Redistributions of source code must retain the above
|
|
|
|
|
* copyright notice, this list of conditions and the
|
|
|
|
|
* following disclaimer.
|
|
|
|
|
* * Redistributions in binary form must reproduce the above
|
|
|
|
|
* copyright notice, this list of conditions and the
|
|
|
|
|
* following disclaimer in the documentation and/or other
|
|
|
|
|
* materials provided with the distribution.
|
|
|
|
|
*
|
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
|
|
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
|
|
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
|
|
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
|
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
|
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
|
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
|
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
|
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
|
|
|
* SUCH DAMAGE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Windows.Forms;
|
|
|
|
|
|
|
|
|
|
using XenAdmin.Controls;
|
|
|
|
|
using XenAdmin.Core;
|
|
|
|
|
using XenAdmin.Actions;
|
|
|
|
|
|
|
|
|
|
using XenAPI;
|
|
|
|
|
using XenAdmin.Dialogs;
|
|
|
|
|
using XenAdmin.Commands;
|
|
|
|
|
|
|
|
|
|
namespace XenAdmin.TabPages
|
|
|
|
|
{
|
|
|
|
|
internal partial class SrStoragePage : BaseTabPage
|
|
|
|
|
{
|
|
|
|
|
private SR sr;
|
2013-07-16 16:41:55 +02:00
|
|
|
|
private bool rebuildRequired;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
private readonly VDIsDataGridViewBuilder dataGridViewBuilder;
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
public SrStoragePage()
|
|
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
|
|
|
{
|
|
|
|
|
dataGridViewVDIs.Columns[i].SortMode = DataGridViewColumnSortMode.Automatic;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
ConnectionsManager.History.CollectionChanged += History_CollectionChanged;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
base.Text = Messages.VIRTUAL_DISKS;
|
|
|
|
|
Properties.Settings.Default.PropertyChanged += Default_PropertyChanged;
|
2013-07-23 14:40:12 +02:00
|
|
|
|
dataGridViewBuilder = new VDIsDataGridViewBuilder(this);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 17:24:01 +02:00
|
|
|
|
private bool disposed;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Clean up any resources being used.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
|
|
|
|
protected override void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!disposed)
|
|
|
|
|
{
|
|
|
|
|
// Deregister listeners.
|
|
|
|
|
SR = null;
|
|
|
|
|
|
|
|
|
|
ConnectionsManager.History.CollectionChanged -= History_CollectionChanged;
|
|
|
|
|
Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged;
|
|
|
|
|
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
|
|
|
|
if (components != null)
|
|
|
|
|
components.Dispose();
|
|
|
|
|
|
|
|
|
|
dataGridViewBuilder.Stop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disposed = true;
|
|
|
|
|
}
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
private void SetupDeprecationBanner()
|
|
|
|
|
{
|
|
|
|
|
if (sr != null && SR.IsIslOrIslLegacy(sr))
|
|
|
|
|
{
|
2014-05-30 12:23:35 +02:00
|
|
|
|
Banner.AppliesToVersion = Messages.XENSERVER_6_5;
|
|
|
|
|
Banner.BannerType = DeprecationBanner.Type.Removal;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
Banner.FeatureName = Messages.ISL_SR;
|
2016-01-28 22:43:15 +01:00
|
|
|
|
Banner.LinkUri = HiddenFeatures.LinkLabelHidden ? null : new Uri(InvisibleMessages.ISL_DEPRECATION_URL);
|
|
|
|
|
Banner.Visible = !HiddenFeatures.LinkLabelHidden;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
Banner.Visible = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SR SR
|
|
|
|
|
{
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
Program.AssertOnEventThread();
|
|
|
|
|
|
|
|
|
|
if (sr != null)
|
|
|
|
|
{
|
|
|
|
|
sr.PropertyChanged -= sr_PropertyChanged;
|
|
|
|
|
sr.Connection.Cache.DeregisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
|
|
|
|
|
sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sr = value;
|
2013-07-23 14:40:12 +02:00
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
if (sr != null)
|
|
|
|
|
{
|
|
|
|
|
sr.PropertyChanged += sr_PropertyChanged;
|
|
|
|
|
sr.Connection.Cache.RegisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
|
|
|
|
|
addVirtualDiskButton.Visible = sr.SupportsVdiCreate();
|
|
|
|
|
sr.Connection.XenObjectsUpdated += Connection_XenObjectsUpdated;
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
BuildList(true);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
SetupDeprecationBanner();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
private void RefreshDataGridView(VDIsData data)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
|
|
|
|
dataGridViewVDIs.SuspendLayout();
|
|
|
|
|
try
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
ColumnVolume.Visible = data.ShowStorageLink;
|
2015-10-28 19:12:31 +01:00
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
// Update existing rows
|
|
|
|
|
foreach (var vdiRow in data.VdiRowsToUpdate)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
vdiRow.RefreshRowDetails();
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
2013-07-15 14:27:47 +02:00
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
// Remove rows for deleted VDIs
|
|
|
|
|
foreach (var vdiRow in data.VdiRowsToRemove)
|
2013-07-15 14:27:47 +02:00
|
|
|
|
{
|
|
|
|
|
dataGridViewVDIs.Rows.RemoveAt(vdiRow.Index);
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
// Add rows for new VDIs
|
|
|
|
|
foreach (var vdi in data.VdisToAdd)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
dataGridViewVDIs.Rows.Add(new VDIRow(vdi));
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (dataGridViewVDIs.SortedColumn != null && dataGridViewVDIs.SortOrder != SortOrder.None)
|
|
|
|
|
dataGridViewVDIs.Sort(dataGridViewVDIs.SortedColumn,
|
2013-07-16 16:41:55 +02:00
|
|
|
|
dataGridViewVDIs.SortOrder == SortOrder.Ascending
|
|
|
|
|
? ListSortDirection.Ascending
|
|
|
|
|
: ListSortDirection.Descending);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
|
|
|
|
dataGridViewVDIs.ResumeLayout();
|
|
|
|
|
}
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
RefreshButtons();
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
private IEnumerable<VDIRow> GetCurrentVDIRows()
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
return dataGridViewVDIs.Rows.OfType<VDIRow>();
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
private void BuildList(bool reset)
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
Program.AssertOnEventThread();
|
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
if (sr == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
dataGridViewBuilder.AddRequest(new RefreshGridRequest(sr, reset));
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
private SelectedItemCollection SelectedVDIs
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
List<SelectedItem> vdis = new List<SelectedItem>();
|
|
|
|
|
foreach (DataGridViewRow r in dataGridViewVDIs.SelectedRows)
|
|
|
|
|
{
|
|
|
|
|
VDIRow row = r as VDIRow;
|
2013-07-15 14:27:47 +02:00
|
|
|
|
if (row != null)
|
|
|
|
|
vdis.Add(new SelectedItem(row.VDI));
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
return new SelectedItemCollection(vdis);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-25 13:38:33 +01:00
|
|
|
|
private void UnregisterHandlers()
|
|
|
|
|
{
|
|
|
|
|
ConnectionsManager.History.CollectionChanged -= History_CollectionChanged;
|
|
|
|
|
Properties.Settings.Default.PropertyChanged -= Default_PropertyChanged;
|
|
|
|
|
|
|
|
|
|
if (sr != null)
|
|
|
|
|
{
|
|
|
|
|
sr.PropertyChanged -= sr_PropertyChanged;
|
|
|
|
|
sr.Connection.Cache.DeregisterBatchCollectionChanged<VDI>(VDI_BatchCollectionChanged);
|
|
|
|
|
sr.Connection.XenObjectsUpdated -= Connection_XenObjectsUpdated;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void PageHidden()
|
|
|
|
|
{
|
|
|
|
|
UnregisterHandlers();
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
#region events
|
|
|
|
|
void History_CollectionChanged(object sender, CollectionChangeEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
Program.BeginInvoke(Program.MainWindow, () =>
|
2017-07-14 19:08:54 +02:00
|
|
|
|
{
|
|
|
|
|
SrRefreshAction a = e.Element as SrRefreshAction;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2017-07-14 19:08:54 +02:00
|
|
|
|
if (e.Action == CollectionChangeAction.Add)
|
|
|
|
|
{
|
|
|
|
|
if (a != null)
|
|
|
|
|
a.Completed += a_Completed;
|
|
|
|
|
else
|
|
|
|
|
return;
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2017-07-14 19:08:54 +02:00
|
|
|
|
if (e.Action == CollectionChangeAction.Remove)
|
|
|
|
|
{
|
|
|
|
|
if (a != null)
|
|
|
|
|
{
|
|
|
|
|
a.Completed -= a_Completed;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var range = e.Element as List<SrRefreshAction>;
|
|
|
|
|
if (range != null)
|
|
|
|
|
{
|
|
|
|
|
foreach (var sra in range)
|
|
|
|
|
sra.Completed -= a_Completed;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2017-07-14 19:08:54 +02:00
|
|
|
|
RefreshButtons();
|
|
|
|
|
});
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 15:28:21 +02:00
|
|
|
|
void a_Completed(ActionBase sender)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2013-07-16 16:41:55 +02:00
|
|
|
|
Program.Invoke(Program.MainWindow, RefreshButtons);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Connection_XenObjectsUpdated(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (rebuildRequired)
|
2013-07-23 14:40:12 +02:00
|
|
|
|
BuildList(false);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
|
|
|
|
rebuildRequired = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void sr_PropertyChanged(object sender1, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.PropertyName == "VDIs")
|
|
|
|
|
rebuildRequired = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void VDI_BatchCollectionChanged(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
rebuildRequired = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Default_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.PropertyName != "ShowHiddenVMs")
|
|
|
|
|
return;
|
2015-04-16 15:17:02 +02:00
|
|
|
|
Program.Invoke(this, () => BuildList(false));
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
#region datagridvie wevents
|
|
|
|
|
private void DataGridViewObject_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
if (e.Column.Index == ColumnName.Index)
|
2016-08-26 11:06:30 +02:00
|
|
|
|
{
|
|
|
|
|
var vdi1 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
|
|
|
|
|
var vdi2 = ((VDIRow) dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
|
|
|
|
|
|
2016-08-31 17:42:01 +02:00
|
|
|
|
e.SortResult = vdi1.CompareTo(vdi2);
|
2016-08-26 11:06:30 +02:00
|
|
|
|
e.Handled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
if (e.Column.Index == ColumnDesc.Index)
|
2016-08-26 11:06:30 +02:00
|
|
|
|
{
|
|
|
|
|
var vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
|
|
|
|
|
var vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
|
|
|
|
|
|
|
|
|
|
var descCompare = StringUtility.NaturalCompare(vdi1.Description, vdi2.Description);
|
|
|
|
|
if (descCompare != 0)
|
|
|
|
|
{
|
|
|
|
|
e.SortResult = descCompare;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2016-08-31 17:42:01 +02:00
|
|
|
|
var refCompare = string.Compare(vdi1.opaque_ref, vdi2.opaque_ref, StringComparison.Ordinal);
|
|
|
|
|
e.SortResult = refCompare;
|
2016-08-26 11:06:30 +02:00
|
|
|
|
}
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
if (e.Column.Index == ColumnSize.Index)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
|
|
|
|
VDI vdi1 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex1]).VDI;
|
|
|
|
|
VDI vdi2 = ((VDIRow)dataGridViewVDIs.Rows[e.RowIndex2]).VDI;
|
|
|
|
|
long diff = vdi1.virtual_size - vdi2.virtual_size;
|
|
|
|
|
e.SortResult =
|
|
|
|
|
diff > 0 ? 1 :
|
|
|
|
|
diff < 0 ? -1 :
|
|
|
|
|
0;
|
|
|
|
|
e.Handled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void dataGridViewVDIs_SelectedIndexChanged(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
RefreshButtons();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void dataGridViewVDIs_KeyUp(object sender, KeyEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.KeyCode != Keys.Apps)
|
|
|
|
|
return;
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
if (dataGridViewVDIs.SelectedRows.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
// 3 is the defaul control margin
|
|
|
|
|
contextMenuStrip1.Show(dataGridViewVDIs, 3, dataGridViewVDIs.ColumnHeadersHeight + 3);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
DataGridViewRow row = dataGridViewVDIs.SelectedRows[0];
|
|
|
|
|
contextMenuStrip1.Show(dataGridViewVDIs, 3, row.Height * (row.Index + 2));
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void dataGridViewVDIs_MouseUp(object sender, MouseEventArgs e)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
DataGridView.HitTestInfo hitTestInfo = dataGridViewVDIs.HitTest(e.X, e.Y);
|
|
|
|
|
|
|
|
|
|
if (hitTestInfo.Type == DataGridViewHitTestType.None)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
dataGridViewVDIs.ClearSelection();
|
|
|
|
|
}
|
|
|
|
|
else if (hitTestInfo.Type == DataGridViewHitTestType.Cell && e.Button == MouseButtons.Right
|
|
|
|
|
&& 0 <= hitTestInfo.RowIndex && hitTestInfo.RowIndex < dataGridViewVDIs.Rows.Count
|
|
|
|
|
&& !dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected)
|
|
|
|
|
{
|
|
|
|
|
// Select the row that the user right clicked on (similiar to outlook) if it's not already in the selection
|
|
|
|
|
// (avoids clearing a multiselect if you right click inside it)
|
|
|
|
|
// Check if the CurrentCell is the cell the user right clicked on (but the row is not Selected) [CA-64954]
|
|
|
|
|
// This happens when the grid is initially shown: the current cell is the first cell in the first column, but the row is not selected
|
|
|
|
|
|
|
|
|
|
if (dataGridViewVDIs.CurrentCell == dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex])
|
|
|
|
|
dataGridViewVDIs.Rows[hitTestInfo.RowIndex].Selected = true;
|
|
|
|
|
else
|
|
|
|
|
dataGridViewVDIs.CurrentCell = dataGridViewVDIs[hitTestInfo.ColumnIndex, hitTestInfo.RowIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((hitTestInfo.Type == DataGridViewHitTestType.None || hitTestInfo.Type == DataGridViewHitTestType.Cell)
|
|
|
|
|
&& e.Button == MouseButtons.Right)
|
|
|
|
|
{
|
|
|
|
|
contextMenuStrip1.Show(dataGridViewVDIs, new Point(e.X, e.Y));
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-14 10:06:19 +02:00
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Button and context menu population
|
|
|
|
|
private void contextMenuStrip_Opening(object sender, CancelEventArgs e)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
bool rescan = buttonRescan.Enabled;
|
|
|
|
|
bool add = addVirtualDiskButton.Enabled;
|
|
|
|
|
bool move = buttonMove.Enabled;
|
|
|
|
|
bool delete = RemoveButton.Enabled;
|
|
|
|
|
bool edit = EditButton.Enabled;
|
|
|
|
|
|
|
|
|
|
if (!(rescan || add || move || delete || edit))
|
|
|
|
|
{
|
|
|
|
|
e.Cancel = true;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
return;
|
2016-09-14 10:06:19 +02:00
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
rescanToolStripMenuItem.Visible = rescan;
|
|
|
|
|
addToolStripMenuItem.Visible = add;
|
|
|
|
|
moveVirtualDiskToolStripMenuItem.Visible = move;
|
|
|
|
|
deleteVirtualDiskToolStripMenuItem.Visible = delete;
|
|
|
|
|
editVirtualDiskToolStripMenuItem.Visible = edit;
|
|
|
|
|
toolStripSeparator1.Visible = (rescan || add || move || delete) && edit;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-28 10:50:23 +02:00
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
|
|
|
|
private void RefreshButtons()
|
|
|
|
|
{
|
|
|
|
|
SelectedItemCollection vdis = SelectedVDIs;
|
|
|
|
|
|
|
|
|
|
// Delete button
|
2016-09-14 10:06:19 +02:00
|
|
|
|
// The user can see that this disk is attached to more than one VMs. Allow deletion of multiple VBDs (non default behaviour),
|
|
|
|
|
// but don't allow them to be deleted if a running vm is using the disk (default behaviour).
|
|
|
|
|
|
|
|
|
|
DeleteVirtualDiskCommand deleteCmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis) {AllowMultipleVBDDelete = true};
|
2013-06-24 13:41:48 +02:00
|
|
|
|
if (deleteCmd.CanExecute())
|
|
|
|
|
{
|
|
|
|
|
RemoveButton.Enabled = true;
|
|
|
|
|
RemoveButtonContainer.RemoveAll();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
RemoveButton.Enabled = false;
|
|
|
|
|
RemoveButtonContainer.SetToolTip(deleteCmd.ToolTipText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move button
|
2016-09-28 10:50:23 +02:00
|
|
|
|
Command moveCmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, vdis);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
if (moveCmd.CanExecute())
|
|
|
|
|
{
|
|
|
|
|
buttonMove.Enabled = true;
|
|
|
|
|
toolTipContainerMove.RemoveAll();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
buttonMove.Enabled = false;
|
|
|
|
|
toolTipContainerMove.SetToolTip(moveCmd.ToolTipText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rescan button
|
2016-09-14 10:06:19 +02:00
|
|
|
|
if (sr == null || sr.Locked)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
buttonRescan.Enabled = false;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
else if (HelpersGUI.BeingScanned(sr))
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
buttonRescan.Enabled = false;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
toolTipContainerRescan.SetToolTip(Messages.SCAN_IN_PROGRESS_TOOLTIP);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
buttonRescan.Enabled = true;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
toolTipContainerRescan.RemoveAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add VDI button
|
2016-09-14 10:06:19 +02:00
|
|
|
|
addVirtualDiskButton.Enabled = sr != null && !sr.Locked;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
|
|
|
|
// Properties button
|
|
|
|
|
if (vdis.Count == 1)
|
|
|
|
|
{
|
|
|
|
|
VDI vdi = vdis.AsXenObjects<VDI>()[0];
|
2016-09-14 10:06:19 +02:00
|
|
|
|
EditButton.Enabled = sr != null && !sr.Locked && !vdi.is_a_snapshot && !vdi.Locked;
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
EditButton.Enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
#region Actions on Vdis
|
|
|
|
|
|
|
|
|
|
private void Rescan()
|
|
|
|
|
{
|
|
|
|
|
SrRefreshAction a = new SrRefreshAction(sr);
|
|
|
|
|
a.RunAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddVdi()
|
|
|
|
|
{
|
|
|
|
|
if (sr != null)
|
|
|
|
|
Program.MainWindow.ShowPerConnectionWizard(sr.Connection, new NewDiskDialog(sr.Connection, sr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MoveSelectedVdis()
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
|
|
|
|
SelectedItemCollection vdis = SelectedVDIs;
|
2016-09-28 10:50:23 +02:00
|
|
|
|
Command cmd = MoveVirtualDiskDialog.MoveMigrateCommand(Program.MainWindow, vdis);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
if (cmd.CanExecute())
|
|
|
|
|
cmd.Execute();
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
private void RemoveSelectedVdis()
|
|
|
|
|
{
|
|
|
|
|
SelectedItemCollection vdis = SelectedVDIs;
|
|
|
|
|
DeleteVirtualDiskCommand cmd = new DeleteVirtualDiskCommand(Program.MainWindow, vdis) {AllowMultipleVBDDelete = true};
|
|
|
|
|
if (cmd.CanExecute())
|
|
|
|
|
cmd.Execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EditSelectedVdis()
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
|
|
|
|
SelectedItemCollection vdis = SelectedVDIs;
|
|
|
|
|
if (vdis.Count != 1)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
VDI vdi = vdis.AsXenObjects<VDI>()[0];
|
|
|
|
|
if (vdi.is_a_snapshot)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
new PropertiesDialog(vdi).ShowDialog(this);
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Button and ToolStripMenuItem handlers
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
private void addVirtualDiskButton_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
AddVdi();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RemoveButton_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
RemoveSelectedVdis();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EditButton_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
EditSelectedVdis();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
private void buttonRescan_Click(object sender, EventArgs e)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
Rescan();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
private void buttonMove_Click(object sender, EventArgs e)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
MoveSelectedVdis();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
|
|
|
|
|
private void rescanToolStripMenuItem_Click(object sender, EventArgs e)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
Rescan();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
private void addToolStripMenuItem_Click(object sender, EventArgs e)
|
2013-07-23 14:40:12 +02:00
|
|
|
|
{
|
2016-09-14 10:06:19 +02:00
|
|
|
|
AddVdi();
|
2013-07-23 14:40:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
private void editVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
EditSelectedVdis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void moveVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
MoveSelectedVdis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void deleteVirtualDiskToolStripMenuItem_Click(object sender, EventArgs e)
|
|
|
|
|
{
|
|
|
|
|
RemoveSelectedVdis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
public class VDIRow : DataGridViewRow
|
|
|
|
|
{
|
2013-07-15 14:27:47 +02:00
|
|
|
|
public VDI VDI { get; private set; }
|
2013-06-24 13:41:48 +02:00
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
public VDIRow(VDI vdi)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
|
|
|
|
VDI = vdi;
|
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
|
|
|
{
|
|
|
|
|
Cells.Add(new DataGridViewTextBoxCell());
|
|
|
|
|
Cells[i].Value = GetCellText(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetCellText(int cellIndex)
|
|
|
|
|
{
|
|
|
|
|
switch (cellIndex)
|
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
return VDI.Name;
|
|
|
|
|
case 1:
|
2015-10-26 17:01:55 +01:00
|
|
|
|
string name;
|
|
|
|
|
return VDI.sm_config.TryGetValue("displayname", out name) ? name : "";
|
2013-06-24 13:41:48 +02:00
|
|
|
|
case 2:
|
|
|
|
|
return VDI.Description;
|
|
|
|
|
case 3:
|
|
|
|
|
return VDI.SizeText;
|
|
|
|
|
case 4:
|
|
|
|
|
return VDI.VMsOfVDI;
|
|
|
|
|
default:
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-07-15 14:27:47 +02:00
|
|
|
|
|
2016-09-14 10:06:19 +02:00
|
|
|
|
public void RefreshRowDetails()
|
2013-07-15 14:27:47 +02:00
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
|
|
|
{
|
|
|
|
|
Cells[i].Value = GetCellText(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-16 16:41:55 +02:00
|
|
|
|
public struct VDIsData
|
|
|
|
|
{
|
|
|
|
|
public List<VDIRow> VdiRowsToUpdate { get; private set; }
|
|
|
|
|
public List<VDIRow> VdiRowsToRemove { get; private set; }
|
|
|
|
|
public List<VDI> VdisToAdd { get; private set; }
|
2015-10-28 19:12:31 +01:00
|
|
|
|
public bool ShowStorageLink { get; private set; }
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2015-10-28 19:12:31 +01:00
|
|
|
|
public VDIsData(List<VDIRow> vdiRowsToUpdate, List<VDIRow> vdiRowsToRemove, List<VDI> vdisToAdd,
|
|
|
|
|
bool showStorageLink) : this()
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
|
|
|
|
VdiRowsToUpdate = vdiRowsToUpdate;
|
|
|
|
|
VdiRowsToRemove = vdiRowsToRemove;
|
|
|
|
|
VdisToAdd = vdisToAdd;
|
2015-10-28 19:12:31 +01:00
|
|
|
|
ShowStorageLink = showStorageLink;
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
public class RefreshGridRequest
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
public SR SR { get; private set; }
|
|
|
|
|
public bool Reset { get; private set; }
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
public RefreshGridRequest(SR sr, bool reset)
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
SR = sr;
|
|
|
|
|
Reset = reset;
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
2013-07-23 14:40:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class VDIsDataGridViewBuilder
|
|
|
|
|
{
|
|
|
|
|
private readonly Control owner;
|
|
|
|
|
private readonly object _locker = new object();
|
|
|
|
|
private BackgroundWorker worker;
|
|
|
|
|
private Queue<RefreshGridRequest> queue = new Queue<RefreshGridRequest>();
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
public VDIsDataGridViewBuilder(Control owner)
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
this.owner = owner;
|
|
|
|
|
worker = new BackgroundWorker {WorkerSupportsCancellation = true};
|
|
|
|
|
worker.DoWork += DoWork;
|
|
|
|
|
worker.RunWorkerCompleted += (RunWorkerCompleted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Cancelled || e.Error != null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
lock (_locker)
|
2013-07-16 16:41:55 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
if (queue.Count > 0 && !worker.IsBusy)
|
|
|
|
|
worker.RunWorkerAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private VDIsData GetCurrentData(SR sr, IEnumerable<VDIRow> currentVDIRows)
|
|
|
|
|
{
|
|
|
|
|
List<VDI> vdis =
|
2013-07-16 16:41:55 +02:00
|
|
|
|
sr.Connection.ResolveAll(sr.VDIs).Where(
|
|
|
|
|
vdi =>
|
2015-05-18 16:52:13 +02:00
|
|
|
|
vdi.Show(Properties.Settings.Default.ShowHiddenVMs) &&
|
|
|
|
|
!vdi.IsAnIntermediateStorageMotionSnapshot)
|
2013-07-16 16:41:55 +02:00
|
|
|
|
.ToList();
|
|
|
|
|
|
2015-10-28 19:12:31 +01:00
|
|
|
|
bool showStorageLink = vdis.Find(v => v.sm_config.ContainsKey("SVID")) != null;
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
var vdiRowsToRemove =
|
|
|
|
|
currentVDIRows.Where(vdiRow => !vdis.Contains(vdiRow.VDI)).OrderByDescending(row => row.Index).ToList();
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
var vdiRowsToUpdate = currentVDIRows.Except(vdiRowsToRemove).ToList();
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
var vdisToAdd = vdis.Except(vdiRowsToUpdate.ConvertAll(vdiRow => vdiRow.VDI)).ToList();
|
|
|
|
|
|
2015-10-28 19:12:31 +01:00
|
|
|
|
return new VDIsData(vdiRowsToUpdate, vdiRowsToRemove, vdisToAdd, showStorageLink);
|
2013-07-23 14:40:12 +02:00
|
|
|
|
}
|
2013-07-16 16:41:55 +02:00
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
private void DoWork(object sender, DoWorkEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
RefreshGridRequest refreshRequest;
|
|
|
|
|
lock (_locker)
|
|
|
|
|
{
|
|
|
|
|
refreshRequest = queue.Count > 0 ? queue.Dequeue() : null;
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
2013-07-23 14:40:12 +02:00
|
|
|
|
|
|
|
|
|
if (worker.CancellationPending)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (refreshRequest == null || refreshRequest.SR == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
SrStoragePage page = owner as SrStoragePage;
|
|
|
|
|
if (page == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (refreshRequest.Reset)
|
2015-04-16 15:17:02 +02:00
|
|
|
|
Program.Invoke(owner, page.dataGridViewVDIs.Rows.Clear);
|
2013-07-23 14:40:12 +02:00
|
|
|
|
|
2015-04-16 15:17:02 +02:00
|
|
|
|
Program.Invoke(owner, page.RefreshButtons);
|
2013-07-23 14:40:12 +02:00
|
|
|
|
|
|
|
|
|
IEnumerable<VDIRow> currentVDIRows = Enumerable.Empty<VDIRow>();
|
2015-04-16 15:17:02 +02:00
|
|
|
|
Program.Invoke(owner, () => currentVDIRows = page.GetCurrentVDIRows());
|
2013-07-23 14:40:12 +02:00
|
|
|
|
VDIsData data = GetCurrentData(refreshRequest.SR, currentVDIRows);
|
|
|
|
|
|
|
|
|
|
if (worker.CancellationPending)
|
|
|
|
|
return;
|
|
|
|
|
|
2015-04-16 15:17:02 +02:00
|
|
|
|
Program.Invoke(owner, () => ((SrStoragePage)owner).RefreshDataGridView(data));
|
2013-07-16 16:41:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-23 14:40:12 +02:00
|
|
|
|
public void AddRequest(RefreshGridRequest refreshGridRequest)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
lock (_locker)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
{
|
2013-07-23 14:40:12 +02:00
|
|
|
|
if (refreshGridRequest.Reset)
|
|
|
|
|
queue.Clear();
|
|
|
|
|
|
|
|
|
|
queue.Enqueue(refreshGridRequest);
|
|
|
|
|
|
|
|
|
|
if (!worker.IsBusy)
|
|
|
|
|
worker.RunWorkerAsync();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2013-07-23 14:40:12 +02:00
|
|
|
|
|
|
|
|
|
public void Stop()
|
|
|
|
|
{
|
|
|
|
|
if (!worker.CancellationPending)
|
|
|
|
|
worker.CancelAsync();
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|