Skip to content

Commit

Permalink
Add support for tags & UI filter support on metadata pages
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Aug 3, 2020
1 parent d6707f4 commit 588db08
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/ServiceStack.Common/MetadataTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ public class MetadataOperationType
public List<string> RequiresAnyRole { get; set; }
public List<string> RequiredPermissions { get; set; }
public List<string> RequiresAnyPermission { get; set; }
public List<string> Tags { get; set; }
}

public class MetadataType : IMeta
Expand Down
6 changes: 6 additions & 0 deletions src/ServiceStack/Host/ServiceMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using ServiceStack.FluentValidation;
using ServiceStack.NativeTypes;
using ServiceStack.NativeTypes.CSharp;
using ServiceStack.Text;
using ServiceStack.Web;

namespace ServiceStack.Host
Expand Down Expand Up @@ -53,6 +54,7 @@ public void Add(Type serviceType, Type requestType, Type responseType)
var authAttrs = reqFilterAttrs.OfType<AuthenticateAttribute>().ToList();
var actions = GetImplementedActions(serviceType, requestType);
authAttrs.AddRange(actions.SelectMany(x => x.AllAttributes<AuthenticateAttribute>()));
var tagAttrs = requestType.AllAttributes<TagAttribute>().ToList();

var operation = new Operation
{
Expand All @@ -69,6 +71,7 @@ public void Add(Type serviceType, Type requestType, Type responseType)
RequiresAnyRole = authAttrs.OfType<RequiresAnyRoleAttribute>().SelectMany(x => x.RequiredRoles).ToList(),
RequiredPermissions = authAttrs.OfType<RequiredPermissionAttribute>().SelectMany(x => x.RequiredPermissions).ToList(),
RequiresAnyPermission = authAttrs.OfType<RequiresAnyPermissionAttribute>().SelectMany(x => x.RequiredPermissions).ToList(),
Tags = tagAttrs,
};

this.OperationsMap[requestType] = operation;
Expand Down Expand Up @@ -668,6 +671,7 @@ public class Operation
public List<string> RequiresAnyRole { get; set; }
public List<string> RequiredPermissions { get; set; }
public List<string> RequiresAnyPermission { get; set; }
public List<TagAttribute> Tags { get; set; }

public List<ITypeValidator> RequestTypeValidationRules { get; private set; }
public List<IValidationRule> RequestPropertyValidationRules { get; private set; }
Expand Down Expand Up @@ -720,6 +724,7 @@ public class OperationDto
public List<string> VisibleTo { get; set; }
public List<string> Actions { get; set; }
public List<string> Routes { get; set; }
public List<string> Tags { get; set; }
}

public class XsdMetadata
Expand Down Expand Up @@ -789,6 +794,7 @@ public static OperationDto ToOperationDto(this Operation operation)
ServiceName = operation.ServiceType.GetOperationName(),
Actions = operation.Actions,
Routes = operation.Routes.Map(x => x.Path),
Tags = operation.Tags.Map(x => x.Name),
};

if (operation.RestrictTo != null)
Expand Down
5 changes: 4 additions & 1 deletion src/ServiceStack/Metadata/IndexOperationsControl.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ServiceStack.Host;
using ServiceStack.Text;
Expand Down Expand Up @@ -31,7 +32,9 @@ public string RenderRow(string operationName)
var icons = CreateIcons(op);

var opTemplate = StringBuilderCache.Allocate();
opTemplate.Append("<tr><th>" + icons + "{0}</th>");
opTemplate.Append($"<tr><th data-tags=\"" +
op.Tags.Map(x => x.Name).Join(",") +
"\">" + icons + "{0}</th>");
foreach (var config in MetadataConfig.AvailableFormatConfigs)
{
var uri = baseUrl.CombineWith(config.DefaultMetadataUri);
Expand Down
10 changes: 9 additions & 1 deletion src/ServiceStack/Metadata/OperationControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading.Tasks;
using ServiceStack.Host;
using ServiceStack.Text;
using ServiceStack.Web;

namespace ServiceStack.Metadata
Expand Down Expand Up @@ -59,14 +60,21 @@ public virtual string RequestUri
public virtual Task RenderAsync(Stream output)
{
var baseUrl = HttpRequest.ResolveAbsoluteUrl("~/");
var sbTags = StringBuilderCache.Allocate();
Operation.Tags.Each(x => sbTags.Append($"<span><b>{x.Name}</b></span>"));
var tagsHtml = sbTags.Length > 0
? "<div class=\"tags\">" + StringBuilderCache.ReturnAndFree(sbTags) + "</div>"
: "";

var renderedTemplate = Templates.HtmlTemplates.Format(Templates.HtmlTemplates.GetOperationControlTemplate(),
Title,
baseUrl.CombineWith(MetadataConfig.DefaultMetadataUri),
ContentFormat.ToUpper(),
OperationName,
GetHttpRequestTemplate(),
ResponseTemplate,
MetadataHtml);
MetadataHtml,
tagsHtml);

return output.WriteAsync(renderedTemplate);
}
Expand Down
1 change: 1 addition & 0 deletions src/ServiceStack/NativeTypes/NativeTypesMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public MetadataTypes GetMetadataTypes(IRequest req, Func<Operation, bool> predic
RequiresAnyRole = operation.RequiresAnyRole,
RequiredPermissions = operation.RequiredPermissions,
RequiresAnyPermission = operation.RequiresAnyPermission,
Tags = operation.Tags.Count > 0 ? operation.Tags.Map(x => x.Name) : null,
};
opType.Request.RequestType = opType;
metadata.Operations.Add(opType);
Expand Down
76 changes: 65 additions & 11 deletions src/ServiceStack/Templates/IndexOperations.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
#filter td {
text-align: right;
}
#filter div {
#filter div.txt {
margin: -35px 0 0 0;
}
.error-popup {
Expand All @@ -192,6 +192,28 @@
font-size: 18px;
display: block;
}
div.tags {
color: rgb(44, 87, 119);
user-select: none;
}
div.tags span {
background: rgb(225, 236, 244);
border-radius: 3px;
margin: 2px 0 5px 2px;
display: inline-block;
cursor: pointer;
}
div.tags span:hover {
background: rgb(209, 229, 241);
}
div.tags span.active {
background: #428bca;
color: #fff;
}
div.tags b {
font-weight: normal;
padding: 2px 5px;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -224,7 +246,18 @@ <h1>{0}</h1>
if (table) {
var cols = table.querySelectorAll("tr:first-child td").length + 1; //th
var isIE9 = document.documentMode <= 9;
var html = '<div><input type="text" placeholder="filter" /></div>';
var tagNames = []
table.querySelectorAll('[data-tags]').forEach(function (x) {
Array.prototype.push.apply(tagNames,x.getAttribute('data-tags').split(','));
});
var tagsHtml = tagNames.filter(function(value,index,self) {
return value && self.indexOf(value) === index;
}).map(function(x) { return "<span><b>" + x + "</b></span>"}).join('');
if (tagsHtml !== "") {
tagsHtml = '<div class="tags">' + tagsHtml + '</div>';
}
var html = '<div class="txt"><input type="text" placeholder="filter" />' + tagsHtml + '</div>';

if (!isIE9) {
table.insertAdjacentHTML("afterbegin",
'<thead id="filter"><tr><td colspan="' + cols + '">' + html + '</td></tr></thead>');
Expand All @@ -239,18 +272,39 @@ <h1>{0}</h1>
thead.appendChild(tr);
table.insertBefore(thead, table.firstChild);
}

var tagEls = document.querySelectorAll('.tags span');
var input = document.querySelectorAll("#filter input")[0];
function filter() {
var activeTagEl = document.querySelector('.tags span.active b');
var activeTag = activeTagEl && activeTagEl.innerHTML;

var rows = document.querySelectorAll("tbody tr");
for (var i = 0; i < rows.length; i++) {
var th = rows[i].firstChild;
var thTags = th.getAttribute('data-tags').split(',').filter(function(x){ return !!x});
rows[i].style.display = th.innerHTML.toLowerCase()
.indexOf(input.value.toLowerCase()) >= 0
&& (!activeTag || thTags.indexOf(activeTag) >= 0)
? 'block'
: 'none';
}
}
if (input) {
input.onkeyup = function () {
var rows = document.querySelectorAll("tbody tr");
for (var i = 0; i < rows.length; i++) {
rows[i].style.display = rows[i].firstChild.innerHTML.toLowerCase()
.indexOf(input.value.toLowerCase()) >= 0
? 'block'
: 'none';
}
};
input.onkeyup = filter;
}

function selectTag() {
var $this = this;
tagEls.forEach(function(el) {
console.log(el, $this);
el.className = el === $this && $this.className !== 'active' ? 'active' : '';
});
filter();
}
tagEls.forEach(function(el) {
el.onclick = selectTag;
});
}
}
</script>
Expand Down
19 changes: 19 additions & 0 deletions src/ServiceStack/Templates/OperationControl.html
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@
line-height: 20px;
color: #444;
}
div.tags {
color: rgb(44, 87, 119);
user-select: none;
margin: -15px 0 0 0;
}
div.tags span {
background: rgb(225, 236, 244);
border-radius: 3px;
margin: 2px 0 5px 2px;
display: inline-block;
}
div.tags span:hover {
background: rgb(209, 229, 241);
}
div.tags b {
font-weight: normal;
padding: 2px 5px;
}
</style>
</head>
<body>
Expand All @@ -177,6 +195,7 @@ <h1>{0}</h1>
<div>
<p><a href="{1}">&lt;back to all web services</a></p>
<h2>{3}</h2>
{7}

{6}

Expand Down
1 change: 1 addition & 0 deletions tests/Check.ServiceModel/Annotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Check.ServiceModel
[Alias("AliasAnnotations")]
[Schema("Annotations.dbo")]
[NamedConnection("AnnotationsDb")]
[Tag("web"),Tag("mobile"),Tag("desktop")]
public class HelloAnnotations : IReturn<HelloAnnotations>
{
[Display(AutoGenerateField = false, AutoGenerateFilter = true, ShortName = "Id", Order = 1)]
Expand Down
11 changes: 9 additions & 2 deletions tests/Check.ServiceModel/CodeGenTestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Check.ServiceModel.Operations
{
[System.ComponentModel.Description("Description for HelloACodeGenTest")]
[Tag("web")]
public class HelloACodeGenTest
{
[Description("Description for FirstField")]
Expand All @@ -19,6 +20,7 @@ public class HelloACodeGenTest
}

[DataContract]
[Tag("mobile")]
public class HelloACodeGenTestResponse
{
[DataMember]
Expand All @@ -32,6 +34,7 @@ public class HelloACodeGenTestResponse

[Route("/hello")]
[Route("/hello/{Name}")]
[Tag("web"),Tag("mobile")]
public class Hello
{
[Required]
Expand All @@ -43,8 +46,8 @@ public class HelloResponse
{
public string Result { get; set; }
}


[Tag("desktop")]
public class HelloWithNestedClass : IReturn<HelloResponse>
{
public string Name { get; set; }
Expand Down Expand Up @@ -72,21 +75,25 @@ public class ArrayResult
public string Result { get; set; }
}

[Tag("web")]
public class HelloList : IReturn<List<ListResult>>
{
public List<string> Names { get; set; }
}

[Tag("mobile")]
public class HelloReturnList : IReturn<List<OnlyInReturnListArg>>
{
public List<string> Names { get; set; }
}

[Tag("desktop")]
public class HelloArray : IReturn<ArrayResult[]>
{
public List<string> Names { get; set; }
}

[Tag("mobile"),Tag("desktop")]
public class HelloExisting : IReturn<HelloExistingResponse>
{
public List<string> Names { get; set; }
Expand Down
4 changes: 4 additions & 0 deletions tests/CheckWebCore/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,14 @@ public class HelloResponse
}

[Route("/testauth")]
[Tag("mobile")]
public class TestAuth : IReturn<TestAuth> {}

[Route("/session")]
public class Session : IReturn<AuthUserSession> {}

[Route("/throw")]
[Tag("desktop")]
public class Throw {}

[Route("/api/data/import/{Month}", "POST")]
Expand All @@ -258,6 +260,7 @@ public class ResponseBase<T>
{
public T Result { get; set; }
}
[Tag("web")]
public class Campaign : IReturn<ResponseBase<Dictionary<string, List<object>>>>
{
public int Id { get; set; }
Expand Down Expand Up @@ -304,6 +307,7 @@ public class HelloBodyResponse
Verbs = "POST")]
[ApiResponse(HttpStatusCode.Unauthorized, "You were unauthorized to call this service")]
//[Restrict(VisibleLocalhostOnly = true)]
[Tag("web"),Tag("mobile"),Tag("desktop")]
public class CreateBookings : CreateBookingBase ,IReturn<CreateBookingsResponse>
{

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
body {
background-color:white;
color:#000000;
font-family: "Helvetica Neue", Helvetica, Verdana, Arial;
font-family: "Helvetica Neue", Helvetica, Verdana, Arial,serif;
margin: 0;
font-size: 13px;
}
Expand Down Expand Up @@ -142,7 +142,7 @@
.example pre {
background-color: #E5E5CC;
border: 1px solid #F0F0E0;
font-family: Courier New;
font-family: Courier New,monospace;
font-size: 15px;
padding: 5px;
margin-right: 15px;
Expand Down

0 comments on commit 588db08

Please sign in to comment.