Skip to content

Commit 5e1aeab

Browse files
authored
Add namespace and package prefix search tools to namespace reservation admin panel (#9055)
Progress on NuGet/Engineering#4269
1 parent 1ca22a8 commit 5e1aeab

8 files changed

Lines changed: 331 additions & 112 deletions

File tree

src/NuGetGallery/Areas/Admin/Controllers/ReservedNamespaceController.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Data.Entity;
56
using System.Linq;
67
using System.Web.Mvc;
78
using System.Threading.Tasks;
@@ -13,19 +14,23 @@ namespace NuGetGallery.Areas.Admin.Controllers
1314
{
1415
public class ReservedNamespaceController : AdminControllerBase
1516
{
16-
private IReservedNamespaceService _reservedNamespaceService;
17+
private readonly IReservedNamespaceService _reservedNamespaceService;
18+
private readonly IEntityRepository<PackageRegistration> _packageRegistrations;
1719

1820
protected ReservedNamespaceController() { }
1921

20-
public ReservedNamespaceController(IReservedNamespaceService reservedNamespaceService)
22+
public ReservedNamespaceController(
23+
IReservedNamespaceService reservedNamespaceService,
24+
IEntityRepository<PackageRegistration> packageRegistrations)
2125
{
2226
_reservedNamespaceService = reservedNamespaceService ?? throw new ArgumentNullException(nameof(reservedNamespaceService));
27+
_packageRegistrations = packageRegistrations ?? throw new ArgumentNullException(nameof(packageRegistrations));
2328
}
2429

2530
[HttpGet]
2631
public ActionResult Index()
2732
{
28-
return View();
33+
return View(new ReservedNamespaceViewModel());
2934
}
3035

3136
[HttpGet]
@@ -49,6 +54,37 @@ public JsonResult SearchPrefix(string query)
4954
return Json(results, JsonRequestBehavior.AllowGet);
5055
}
5156

57+
[HttpGet]
58+
public ActionResult FindNamespacesByPrefix(string prefix)
59+
{
60+
var namespaces = _reservedNamespaceService
61+
.FindAllReservedNamespacesForPrefix(prefix, getExactMatches: false)
62+
.OrderBy(x => x.Value)
63+
.ThenBy(x => x.IsPrefix)
64+
.ToList();
65+
var model = new ReservedNamespaceViewModel { ReservedNamespacesQuery = prefix, ReservedNamespaces = namespaces };
66+
return View(nameof(Index), model);
67+
}
68+
69+
[HttpGet]
70+
public ActionResult FindPackagesByPrefix(string prefix)
71+
{
72+
if (string.IsNullOrWhiteSpace(prefix))
73+
{
74+
return RedirectToAction(nameof(Index));
75+
}
76+
77+
var packageRegistrations = _packageRegistrations
78+
.GetAll()
79+
.Include(x => x.Owners)
80+
.Include(x => x.ReservedNamespaces)
81+
.Where(x => x.Id.StartsWith(prefix))
82+
.OrderBy(x => x.Id)
83+
.ToList();
84+
var model = new ReservedNamespaceViewModel { PackageRegistrationsQuery = prefix, PackageRegistrations = packageRegistrations };
85+
return View(nameof(Index), model);
86+
}
87+
5288
[HttpPost]
5389
[ValidateAntiForgeryToken]
5490
public async Task<JsonResult> AddNamespace(ReservedNamespace newNamespace)
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
5+
using NuGet.Services.Entities;
6+
47
namespace NuGetGallery.Areas.Admin.ViewModels
58
{
69
public sealed class ReservedNamespaceViewModel
710
{
8-
/// <summary>
9-
/// Prefix search query.
10-
/// </summary>
11-
public string PrefixSearchQuery { get; set; }
11+
public string ReservedNamespacesQuery { get; set; }
12+
public IReadOnlyList<ReservedNamespace> ReservedNamespaces { get; set; }
13+
public string PackageRegistrationsQuery { get; set; }
14+
public IReadOnlyList<PackageRegistration> PackageRegistrations { get; set; }
1215
}
1316
}

src/NuGetGallery/Areas/Admin/Views/ReservedNamespace/Index.cshtml

Lines changed: 188 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,75 +6,196 @@
66
@ViewHelpers.AjaxAntiForgeryToken(Html)
77

88
<section role="main" class="container main-container">
9-
<div class="message-container" data-bind="visible: message">
10-
@ViewHelpers.AlertInfo(@<text><span class="message" data-bind="text: message"></span></text>)
11-
</div>
12-
<h2>Reserve Namespace</h2>
13-
<form data-bind="submit: prefixSearch">
14-
<div class="form-horizontal">
15-
<input type="text" placeholder="Search for a prefix" autocomplete="on" autofocus data-bind="value: prefixSearchQuery" />
16-
<input type="submit" value="Search Prefix" title="Search Prefix" />
17-
</div>
18-
</form><br />
19-
20-
<div data-bind="visible: allPrefixResults().length > 0">
21-
@using (Html.BeginForm())
22-
{
23-
<div id="prefixResult" data-bind="foreach: allPrefixResults">
24-
<table aria-label="Reserved Namespaces Search Results">
25-
<tr>
26-
<td>
27-
<span style="font-weight: bold">Details:</span>
28-
<div style="margin-left: 20px">
29-
<div>
30-
<span style="font-weight: bold">Namespace: </span>
31-
<span data-bind="text: $data.prefix.Value" />
32-
</div>
33-
<div>
34-
<label>
35-
<input type="checkbox" data-bind="checked: $data.prefix.IsSharedNamespace, disable: $data.isExisting" />IsSharedNamespace
36-
</label>
37-
</div>
38-
<div>
39-
<label>
40-
<input type="checkbox" data-bind="checked: $data.prefix.IsPrefix, disable: $data.isExisting" value="IsPrefix" />IsPrefix
41-
</label>
42-
</div>
43-
<div>
44-
<span style="font-weight: bold">Matching Pattern: </span>
45-
<span data-bind="text: $data.pattern" />
46-
</div>
47-
<!-- ko if: $data.isExisting -->
48-
<input style="margin-top: 5px" type="submit" value="Deallocate Namespace" title="Delete this namespace"
49-
data-bind="click: $parent.managePrefix.bind(this, $data.prefix, false)" />
50-
<!-- /ko -->
51-
<!-- ko ifnot: $data.isExisting -->
52-
<input style="margin-top: 5px" type="submit" value="Reserve Namespace" title="Add this namespace"
53-
data-bind="click: $parent.managePrefix.bind(this, $data.prefix, true)" />
54-
<!-- /ko -->
55-
</div>
56-
</td>
57-
<td style="vertical-align: top">
58-
<!-- ko if: $data.isExisting -->
59-
<span style="font-weight: bold">Owners:</span>
60-
<div style="margin-left: 20px">
61-
<div id="prefixOwners" data-bind="foreach: $data.owners">
62-
<div style="margin: 5px">
63-
<a href="#" target="_blank" data-bind="text: $data, attr: { href: $root.generateUserUrl($data) }"></a>
64-
<input style="float: right" type="button" value="Remove" title="Remove this owner" data-bind="click: $root.removeOwner.bind(this, $data, $parent.prefix)" />
9+
10+
<div class="row">
11+
<div class="col-xs-12">
12+
<div class="message-container" data-bind="visible: message">
13+
@ViewHelpers.AlertInfo(@<text><span class="message" data-bind="text: message"></span></text>)
14+
</div>
15+
<h2>Reserve namespace</h2>
16+
<form data-bind="submit: prefixSearch">
17+
<div class="form-horizontal">
18+
<input type="text" placeholder="Specific namespace" autocomplete="on" autofocus data-bind="value: prefixSearchQuery" />
19+
<input type="submit" value="Search" title="Search" />
20+
</div>
21+
</form>
22+
<br />
23+
24+
<div data-bind="visible: allPrefixResults().length > 0">
25+
@using (Html.BeginForm())
26+
{
27+
<div id="prefixResult" data-bind="foreach: allPrefixResults">
28+
<table aria-label="Reserved Namespaces Search Results">
29+
<tr>
30+
<td>
31+
<span style="font-weight: bold">Details:</span>
32+
<div style="margin-left: 20px">
33+
<div>
34+
<span style="font-weight: bold">Namespace: </span>
35+
<span data-bind="text: $data.prefix.Value" />
36+
</div>
37+
<div>
38+
<label>
39+
<input type="checkbox" data-bind="checked: $data.prefix.IsSharedNamespace, disable: $data.isExisting" />IsSharedNamespace
40+
</label>
41+
</div>
42+
<div>
43+
<label>
44+
<input type="checkbox" data-bind="checked: $data.prefix.IsPrefix, disable: $data.isExisting" value="IsPrefix" />IsPrefix
45+
</label>
46+
</div>
47+
<div>
48+
<span style="font-weight: bold">Matching Pattern: </span>
49+
<span data-bind="text: $data.pattern" />
50+
</div>
51+
<!-- ko if: $data.isExisting -->
52+
<input style="margin-top: 5px" type="submit" value="Deallocate Namespace" title="Delete this namespace"
53+
data-bind="click: $parent.managePrefix.bind(this, $data.prefix, false)" />
54+
<!-- /ko -->
55+
<!-- ko ifnot: $data.isExisting -->
56+
<input style="margin-top: 5px" type="submit" value="Reserve Namespace" title="Add this namespace"
57+
data-bind="click: $parent.managePrefix.bind(this, $data.prefix, true)" />
58+
<!-- /ko -->
6559
</div>
66-
</div>
67-
<div style="margin: 5px">
68-
<input type="text" placeholder="Enter username" autocomplete="off" data-bind="value: $parent.newOwner, valueUpdate: 'afterkeydown'" />
69-
<input type="button" value="Add" title="Add new owner for this prefix" data-bind="click: $parent.addOwner.bind(this, $data.prefix), enable: $parent.newOwner().length > 0" />
70-
</div>
71-
</div>
72-
<!-- /ko -->
73-
</td>
74-
</tr>
75-
</table>
60+
</td>
61+
<td style="vertical-align: top">
62+
<!-- ko if: $data.isExisting -->
63+
<span style="font-weight: bold">Owners:</span>
64+
<div style="margin-left: 20px">
65+
<div id="prefixOwners" data-bind="foreach: $data.owners">
66+
<div style="margin: 5px">
67+
<a href="#" target="_blank" data-bind="text: $data, attr: { href: $root.generateUserUrl($data) }"></a>
68+
<input style="float: right" type="button" value="Remove" title="Remove this owner" data-bind="click: $root.removeOwner.bind(this, $data, $parent.prefix)" />
69+
</div>
70+
</div>
71+
<div style="margin: 5px">
72+
<input type="text" placeholder="Enter username" autocomplete="off" data-bind="value: $parent.newOwner, valueUpdate: 'afterkeydown'" />
73+
<input type="button" value="Add" title="Add new owner for this prefix" data-bind="click: $parent.addOwner.bind(this, $data.prefix), enable: $parent.newOwner().length > 0" />
74+
</div>
75+
</div>
76+
<!-- /ko -->
77+
</td>
78+
</tr>
79+
</table>
80+
</div>
81+
}
7682
</div>
77-
}
83+
</div>
84+
</div>
85+
86+
<div class="row">
87+
<div class="col-xs-12">
88+
<h2>Find namespaces by prefix</h2>
89+
<form method="get" action="@Url.Action("FindNamespacesByPrefix")">
90+
<div class="form-horizontal">
91+
<input type="text" placeholder="Namespace prefix" autocomplete="on" name="prefix" value="@Model.ReservedNamespacesQuery" />
92+
<input type="submit" value="Search" />
93+
</div>
94+
</form>
95+
<br />
96+
@if (Model.ReservedNamespaces != null)
97+
{
98+
if (Model.ReservedNamespaces.Count == 0)
99+
{
100+
<p>No reserved namespaces found.</p>
101+
}
102+
else
103+
{
104+
<table class="table">
105+
<thead>
106+
<tr>
107+
<th>Value</th>
108+
<th>IsSharedNamespace</th>
109+
<th>IsPrefix</th>
110+
<th>Owners</th>
111+
</tr>
112+
</thead>
113+
<tbody>
114+
@foreach (var rn in Model.ReservedNamespaces)
115+
{
116+
<tr>
117+
<td>@(rn.Value + (rn.IsPrefix ? "*" : ""))</td>
118+
<td>@rn.IsSharedNamespace</td>
119+
<td>@rn.IsPrefix</td>
120+
<td>
121+
@foreach (var owner in rn.Owners)
122+
{
123+
<a href="@Url.User(owner)">@owner.Username</a>
124+
}
125+
</td>
126+
</tr>
127+
}
128+
</tbody>
129+
</table>
130+
}
131+
}
132+
</div>
133+
</div>
134+
135+
<div class="row">
136+
<div class="col-xs-12">
137+
<h2>Find package registrations by prefix</h2>
138+
<form method="get" action="@Url.Action("FindPackagesByPrefix")">
139+
<div class="form-horizontal">
140+
<input type="text" placeholder="Package ID prefix" autocomplete="on" name="prefix" value="@Model.PackageRegistrationsQuery" />
141+
<input type="submit" value="Search" />
142+
</div>
143+
</form>
144+
<br />
145+
@if (Model.PackageRegistrations != null)
146+
{
147+
if (Model.PackageRegistrations.Count == 0)
148+
{
149+
<p>No package registrations found.</p>
150+
}
151+
else
152+
{
153+
<table class="table">
154+
<thead>
155+
<tr>
156+
<th>Package ID</th>
157+
<th>IsVerified</th>
158+
<th>Download count</th>
159+
<th>Owners</th>
160+
<th>Reserved Namespaces</th>
161+
</tr>
162+
</thead>
163+
<tbody>
164+
@foreach (var pr in Model.PackageRegistrations)
165+
{
166+
<tr>
167+
<td><a href="@Url.Package(pr.Id)">@pr.Id</a></td>
168+
<td>
169+
@if (pr.IsVerified)
170+
{
171+
<img class="img-responsive"
172+
src="~/Content/gallery/img/reserved-indicator.svg"
173+
alt="Reserved namespace icon"
174+
width="24" height="24"
175+
@ViewHelpers.ImageFallback(Url.Absolute("~/Content/gallery/img/reserved-indicator-256x256.png"))
176+
title="@Strings.ReservedNamespace_ReservedIndicatorTooltip" />
177+
}
178+
</td>
179+
<td>@pr.DownloadCount.ToNuGetNumberString()</td>
180+
<td>
181+
@foreach (var owner in pr.Owners)
182+
{
183+
<a href="@Url.User(owner)">@owner.Username</a>
184+
}
185+
</td>
186+
<td>
187+
@foreach (var rn in pr.ReservedNamespaces)
188+
{
189+
@(rn.Value + (rn.IsPrefix ? "* " : " "))
190+
}
191+
</td>
192+
</tr>
193+
}
194+
</tbody>
195+
</table>
196+
}
197+
}
198+
</div>
78199
</div>
79200
</section>
80201

0 commit comments

Comments
 (0)