History

(1) GSF.Security: Added modeled security tables. (2) GSF.Web: Added security, data context, view template and scripts related to modeled table operations

Ritchie Carroll committed 10 years ago

Commit a02edca1 parent 46782aa32 files · +5897 −0Browse files

Unified · Split

32 files
  1. Source/Libraries/GSF.Core/Collections/CollectionExtensions.cs +11 −0
  2. Source/Libraries/GSF.Security/GSF.Security.csproj +10 −0
  3. Source/Libraries/GSF.Security/Model/AccessLog.cs +58 −0
  4. Source/Libraries/GSF.Security/Model/ApplicationRole.cs +79 −0
  5. Source/Libraries/GSF.Security/Model/ApplicationRoleSecurityGroup.cs +52 −0
  6. Source/Libraries/GSF.Security/Model/ApplicationRoleUserAccount.cs +52 −0
  7. Source/Libraries/GSF.Security/Model/ErrorLog.cs +87 −0
  8. Source/Libraries/GSF.Security/Model/Node.cs +80 −0
  9. Source/Libraries/GSF.Security/Model/SecurityGroup.cs +78 −0
  10. Source/Libraries/GSF.Security/Model/SecurityGroupUserAccount.cs +52 −0
  11. Source/Libraries/GSF.Security/Model/UserAccount.cs +139 −0
  12. Source/Libraries/GSF.Web/GSF.Web.csproj +105 −0
  13. Source/Libraries/GSF.Web/Model/DataContext.cs +955 −0
  14. Source/Libraries/GSF.Web/Model/RecordOperationsCache.cs +91 −0
  15. Source/Libraries/GSF.Web/Model/Scripts/gsf.web.client.js +667 −0
  16. Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js +568 −0
  17. Source/Libraries/GSF.Web/Model/Views/PagedViewModel.cshtml +348 −0
  18. Source/Libraries/GSF.Web/Security/AuthorizationCache.cs +39 −0
  19. Source/Libraries/GSF.Web/Security/AuthorizeControllerRoleAttribute.cs +156 −0
  20. Source/Libraries/GSF.Web/Security/AuthorizeHubRoleAttribute.cs +138 −0
  21. Source/Libraries/GSF.Web/Security/SecurityHub.cs +292 −0
  22. Source/Libraries/GSF.Web/Security/Views/Groups.cshtml +370 −0
  23. Source/Libraries/GSF.Web/Security/Views/Users.cshtml +381 −0
  24. Source/Libraries/GSF.Web/Templating/NamespaceDoc.cs +35 −0
  25. Source/Libraries/GSF.Web/Templating/RazorView.cs +461 −0
  26. Source/Libraries/GSF.Web/Templating/Views/AddCheckBoxField.cshtml +70 −0
  27. Source/Libraries/GSF.Web/Templating/Views/AddDateField.cshtml +109 −0
  28. Source/Libraries/GSF.Web/Templating/Views/AddInputField.cshtml +103 −0
  29. Source/Libraries/GSF.Web/Templating/Views/AddSelectField.cshtml +86 −0
  30. Source/Libraries/GSF.Web/Templating/Views/AddTextAreaField.cshtml +103 −0
  31. Source/Libraries/GSF.Web/WebExtensions.cs +102 −0
  32. Source/Libraries/GSF.Web/packages.config +20 −0
Source/Libraries/GSF.Core/Collections/CollectionExtensions.cs+11−0
@@ -58,6 +58,7 @@ · 57 unmodified lines
5858 using System;
5959 using System.Collections;
6060 using System.Collections.Generic;
61+using System.Collections.Specialized;
6162 using System.Linq;
6263 using System.Text;
6364 using Random = GSF.Security.Cryptography.Random;
@@ -292,6 +293,16 @@ namespace GSF.Collections · 228 unmodified lines
292293 return value;
293294 }
294295
296+ /// <summary>
297+ /// Converts a name/value collection to a dictionary.
298+ /// </summary>
299+ /// <param name="collection">Name/value collection.</param>
300+ /// <returns>Dictionary converted from a name/value collection.</returns>
301+ public static Dictionary<string, string> ToDictionary(this NameValueCollection collection)
302+ {
303+ return collection.AllKeys.ToDictionary(key => key, key => collection[key]);
304+ }
305+
295306 /// <summary>
296307 /// Returns <c>true</c> if any item in <see cref="BitArray"/> is equal to <paramref name="value"/>.
297308 /// </summary>
Source/Libraries/GSF.Security/GSF.Security.csproj+10−0
@@ -122,6 +122,7 @@ · 121 unmodified lines
122122 </PropertyGroup>
123123 <ItemGroup>
124124 <Reference Include="System" />
125+ <Reference Include="System.ComponentModel.DataAnnotations" />
125126 <Reference Include="System.configuration" />
126127 <Reference Include="System.Core">
127128 <RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -144,6 +145,15 @@ · 16 unmodified lines
144145 <ItemGroup>
145146 <Compile Include="AdoSecurityProvider.cs" />
146147 <Compile Include="AdoSecurityCache.cs" />
148+ <Compile Include="Model\AccessLog.cs" />
149+ <Compile Include="Model\ErrorLog.cs" />
150+ <Compile Include="Model\ApplicationRole.cs" />
151+ <Compile Include="Model\ApplicationRoleSecurityGroup.cs" />
152+ <Compile Include="Model\SecurityGroupUserAccount.cs" />
153+ <Compile Include="Model\ApplicationRoleUserAccount.cs" />
154+ <Compile Include="Model\Node.cs" />
155+ <Compile Include="Model\SecurityGroup.cs" />
156+ <Compile Include="Model\UserAccount.cs" />
147157 <Compile Include="UserRoleCache.cs" />
148158 <Compile Include="LdapSecurityProvider.cs" />
149159 <Compile Include="RestrictAccessAttribute.cs" />
Source/Libraries/GSF.Security/Model/AccessLog.cs+58−0
@@ -0,0 +1,58 @@
1+//******************************************************************************************************
2+// AccessLog.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for AccessLog table.
32+ /// </summary>
33+ public class AccessLog
34+ {
35+ /// <summary>
36+ /// Unique ID field.
37+ /// </summary>
38+ [PrimaryKey(true)]
39+ public int ID { get; set; }
40+
41+ /// <summary>
42+ /// User name field.
43+ /// </summary>
44+ [Required]
45+ [StringLength(200)]
46+ public string UserName { get; set; }
47+
48+ /// <summary>
49+ /// Access granted flag field.
50+ /// </summary>
51+ public bool AccessGranted { get; set; }
52+
53+ /// <summary>
54+ /// Created on field.
55+ /// </summary>
56+ public DateTime CreatedOn { get; set; }
57+ }
58+}
Source/Libraries/GSF.Security/Model/ApplicationRole.cs+79−0
@@ -0,0 +1,79 @@
1+//******************************************************************************************************
2+// ApplicationRole.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for ApplicationRole table.
32+ /// </summary>
33+ [PrimaryLabel("Name")]
34+ public class ApplicationRole
35+ {
36+ /// <summary>
37+ /// Unique application role ID field.
38+ /// </summary>
39+ [PrimaryKey(true)]
40+ public Guid ID { get; set; }
41+
42+ /// <summary>
43+ /// Name field.
44+ /// </summary>
45+ [Required]
46+ [StringLength(200)]
47+ public string Name { get; set; }
48+
49+ /// <summary>
50+ /// Description field.
51+ /// </summary>
52+ public string Description { get; set; }
53+
54+ /// <summary>
55+ /// Node ID field.
56+ /// </summary>
57+ public Guid NodeID { get; set; }
58+
59+ /// <summary>
60+ /// Created on field.
61+ /// </summary>
62+ public DateTime CreatedOn { get; set; }
63+
64+ /// <summary>
65+ /// Created by field.
66+ /// </summary>
67+ public string CreatedBy { get; set; }
68+
69+ /// <summary>
70+ /// Updated on field.
71+ /// </summary>
72+ public DateTime UpdatedOn { get; set; }
73+
74+ /// <summary>
75+ /// Updated by field.
76+ /// </summary>
77+ public string UpdatedBy { get; set; }
78+ }
79+}
Source/Libraries/GSF.Security/Model/ApplicationRoleSecurityGroup.cs+52−0
@@ -0,0 +1,52 @@
1+//******************************************************************************************************
2+// ApplicationRoleSecurityGroup.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/03/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+
27+namespace GSF.Security.Model
28+{
29+ /// <summary>
30+ /// Model for ApplicationRoleSecurityGroup table.
31+ /// </summary>
32+ public class ApplicationRoleSecurityGroup
33+ {
34+ /// <summary>
35+ /// Application role ID field.
36+ /// </summary>
37+ [Required]
38+ public Guid ApplicationRoleID
39+ {
40+ get; set;
41+ }
42+
43+ /// <summary>
44+ /// Security group ID field.
45+ /// </summary>
46+ [Required]
47+ public Guid SecurityGroupID
48+ {
49+ get; set;
50+ }
51+ }
52+}
Source/Libraries/GSF.Security/Model/ApplicationRoleUserAccount.cs+52−0
@@ -0,0 +1,52 @@
1+//******************************************************************************************************
2+// ApplicationRoleUserAccount.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+
27+namespace GSF.Security.Model
28+{
29+ /// <summary>
30+ /// Model for ApplicationRoleUserAccount table.
31+ /// </summary>
32+ public class ApplicationRoleUserAccount
33+ {
34+ /// <summary>
35+ /// Application role ID field.
36+ /// </summary>
37+ [Required]
38+ public Guid ApplicationRoleID
39+ {
40+ get; set;
41+ }
42+
43+ /// <summary>
44+ /// User account ID field.
45+ /// </summary>
46+ [Required]
47+ public Guid UserAccountID
48+ {
49+ get; set;
50+ }
51+ }
52+}
Source/Libraries/GSF.Security/Model/ErrorLog.cs+87−0
@@ -0,0 +1,87 @@
1+//******************************************************************************************************
2+// ErrorLog.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for ErrorLog table.
32+ /// </summary>
33+ public class ErrorLog
34+ {
35+ /// <summary>
36+ /// Unique ID field.
37+ /// </summary>
38+ [PrimaryKey(true)]
39+ public int ID
40+ {
41+ get; set;
42+ }
43+
44+ /// <summary>
45+ /// Source field.
46+ /// </summary>
47+ [Required]
48+ [StringLength(200)]
49+ public string Source
50+ {
51+ get; set;
52+ }
53+
54+ /// <summary>
55+ /// Type field.
56+ /// </summary>
57+ public string Type
58+ {
59+ get; set;
60+ }
61+
62+ /// <summary>
63+ /// Message field.
64+ /// </summary>
65+ [Required]
66+ public string Message
67+ {
68+ get; set;
69+ }
70+
71+ /// <summary>
72+ /// Detail field.
73+ /// </summary>
74+ public string Detail
75+ {
76+ get; set;
77+ }
78+
79+ /// <summary>
80+ /// Created on field.
81+ /// </summary>
82+ public DateTime CreatedOn
83+ {
84+ get; set;
85+ }
86+ }
87+}
Source/Libraries/GSF.Security/Model/Node.cs+80−0
@@ -0,0 +1,80 @@
1+//******************************************************************************************************
2+// Node.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for Node table.
32+ /// </summary>
33+ [PrimaryLabel("Name")]
34+ public class Node
35+ {
36+ /// <summary>
37+ /// Unique node ID field.
38+ /// </summary>
39+ [PrimaryKey(true)]
40+ public Guid ID { get; set; }
41+
42+ /// <summary>
43+ /// Name field.
44+ /// </summary>
45+ [Required]
46+ [StringLength(200)]
47+ public string Name { get; set; }
48+
49+ /// <summary>
50+ /// Description field.
51+ /// </summary>
52+ public string Description { get; set; }
53+
54+ /// <summary>
55+ /// Enabled field.
56+ /// </summary>
57+ [InitialValue("true")]
58+ public bool Enabled { get; set; }
59+
60+ /// <summary>
61+ /// Created on field.
62+ /// </summary>
63+ public DateTime CreatedOn { get; set; }
64+
65+ /// <summary>
66+ /// Created by field.
67+ /// </summary>
68+ public string CreatedBy { get; set; }
69+
70+ /// <summary>
71+ /// Updated on field.
72+ /// </summary>
73+ public DateTime UpdatedOn { get; set; }
74+
75+ /// <summary>
76+ /// Updated by field.
77+ /// </summary>
78+ public string UpdatedBy { get; set; }
79+ }
80+}
Source/Libraries/GSF.Security/Model/SecurityGroup.cs+78−0
@@ -0,0 +1,78 @@
1+//******************************************************************************************************
2+// SecurityGroup.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for SecurityGroup table.
32+ /// </summary>
33+ /// <remarks>
34+ /// A record in this table can represent a database defined group with associated users
35+ /// or an Active Directory group that maintains its own users.
36+ /// </remarks>
37+ [PrimaryLabel("Name")]
38+ public class SecurityGroup
39+ {
40+ /// <summary>
41+ /// Unique security group ID field.
42+ /// </summary>
43+ [PrimaryKey(true)]
44+ public Guid ID { get; set; }
45+
46+ /// <summary>
47+ /// Group name field - stores SID for AD authentication.
48+ /// </summary>
49+ [Required]
50+ [StringLength(200)]
51+ public string Name { get; set; }
52+
53+ /// <summary>
54+ /// Description field.
55+ /// </summary>
56+ public string Description { get; set; }
57+
58+ /// <summary>
59+ /// Created on field.
60+ /// </summary>
61+ public DateTime CreatedOn { get; set; }
62+
63+ /// <summary>
64+ /// Created by field.
65+ /// </summary>
66+ public string CreatedBy { get; set; }
67+
68+ /// <summary>
69+ /// Updated on field.
70+ /// </summary>
71+ public DateTime UpdatedOn { get; set; }
72+
73+ /// <summary>
74+ /// Updated by field.
75+ /// </summary>
76+ public string UpdatedBy { get; set; }
77+ }
78+}
Source/Libraries/GSF.Security/Model/SecurityGroupUserAccount.cs+52−0
@@ -0,0 +1,52 @@
1+//******************************************************************************************************
2+// SecurityGroupUserAccount.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/03/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+
27+namespace GSF.Security.Model
28+{
29+ /// <summary>
30+ /// Model for SecurityGroupUserAccount table.
31+ /// </summary>
32+ public class SecurityGroupUserAccount
33+ {
34+ /// <summary>
35+ /// Security group ID field.
36+ /// </summary>
37+ [Required]
38+ public Guid SecurityGroupID
39+ {
40+ get; set;
41+ }
42+
43+ /// <summary>
44+ /// User account ID field.
45+ /// </summary>
46+ [Required]
47+ public Guid UserAccountID
48+ {
49+ get; set;
50+ }
51+ }
52+}
Source/Libraries/GSF.Security/Model/UserAccount.cs+139−0
@@ -0,0 +1,139 @@
1+//******************************************************************************************************
2+// UserAccount.cs - Gbtc
3+//
4+// Copyright � 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/27/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.ComponentModel.DataAnnotations;
26+using GSF.Data.Model;
27+
28+namespace GSF.Security.Model
29+{
30+ /// <summary>
31+ /// Model for UserAccount table.
32+ /// </summary>
33+ /// <remarks>
34+ /// A record in this table can represent a database defined user with associated detail
35+ /// or an Active Directory user that maintains its own user details.
36+ /// </remarks>
37+ [PrimaryLabel("Name")]
38+ public class UserAccount
39+ {
40+ /// <summary>
41+ /// Unique user account ID field.
42+ /// </summary>
43+ [PrimaryKey(true)]
44+ public Guid ID { get; set; }
45+
46+ /// <summary>
47+ /// User name field - stores SID for AD authentication.
48+ /// </summary>
49+ [Label("User Name")]
50+ [Required]
51+ [StringLength(200)]
52+ public string Name { get; set; }
53+
54+ /// <summary>
55+ /// Password field - stores password hash for DB authentication only.
56+ /// </summary>
57+ [Required]
58+ [StringLength(200)]
59+ public string Password { get; set; }
60+
61+ /// <summary>
62+ /// First name field - for DB authentication only.
63+ /// </summary>
64+ [Label("First Name")]
65+ [StringLength(200)]
66+ public string FirstName { get; set; }
67+
68+ /// <summary>
69+ /// Last name field - for DB authentication only.
70+ /// </summary>
71+ [Label("Last Name")]
72+ [StringLength(200)]
73+ public string LastName { get; set; }
74+
75+ /// <summary>
76+ /// Default Node ID field.
77+ /// </summary>
78+ [Required]
79+ public Guid DefaultNodeID { get; set; }
80+
81+ /// <summary>
82+ /// Phone number field - for DB authentication only.
83+ /// </summary>
84+ [StringLength(200)]
85+ public string Phone { get; set; }
86+
87+ /// <summary>
88+ /// E-mail field - for DB authentication only.
89+ /// </summary>
90+ [Label("E-Mail")]
91+ [StringLength(200)]
92+ [RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", ErrorMessage = "Invalid e-mail address.")]
93+ public string Email { get; set; }
94+
95+ /// <summary>
96+ /// Change password on date field - for DB authentication only.
97+ /// </summary>
98+ public DateTime? ChangePasswordOn
99+ {
100+ get; set;
101+ }
102+
103+ /// <summary>
104+ /// Use Active Directory authentication field.
105+ /// </summary>
106+ [Label("Use Active Directory Authentication")]
107+ [InitialValue("true")]
108+ public bool UseADAuthentication { get; set; }
109+
110+ /// <summary>
111+ /// User locked-out field.
112+ /// </summary>
113+ [Label("Locked Out")]
114+ public bool LockedOut
115+ {
116+ get; set;
117+ }
118+
119+ /// <summary>
120+ /// Created on field.
121+ /// </summary>
122+ public DateTime CreatedOn { get; set; }
123+
124+ /// <summary>
125+ /// Created by field.
126+ /// </summary>
127+ public string CreatedBy { get; set; }
128+
129+ /// <summary>
130+ /// Updated on field.
131+ /// </summary>
132+ public DateTime UpdatedOn { get; set; }
133+
134+ /// <summary>
135+ /// Updated by field.
136+ /// </summary>
137+ public string UpdatedBy { get; set; }
138+ }
139+}
Source/Libraries/GSF.Web/GSF.Web.csproj+105−0
@@ -115,15 +115,96 @@ · 114 unmodified lines
115115 <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
116116 </PropertyGroup>
117117 <ItemGroup>
118+ <Reference Include="Microsoft.AspNet.SignalR.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
119+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.SignalR.Core.2.2.0\lib\net45\Microsoft.AspNet.SignalR.Core.dll</HintPath>
120+ <Private>True</Private>
121+ </Reference>
122+ <Reference Include="Microsoft.AspNet.SignalR.SystemWeb, Version=2.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
123+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.SignalR.SystemWeb.2.2.0\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll</HintPath>
124+ <Private>True</Private>
125+ </Reference>
126+ <Reference Include="Microsoft.CSharp" />
127+ <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
128+ <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll</HintPath>
129+ <Private>True</Private>
130+ </Reference>
131+ <Reference Include="Microsoft.Owin.Host.SystemWeb, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
132+ <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.Host.SystemWeb.2.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll</HintPath>
133+ <Private>True</Private>
134+ </Reference>
135+ <Reference Include="Microsoft.Owin.Security, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
136+ <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll</HintPath>
137+ <Private>True</Private>
138+ </Reference>
139+ <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
140+ <HintPath>..\..\Dependencies\NuGet\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
141+ <Private>True</Private>
142+ </Reference>
143+ <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
144+ <HintPath>..\..\Dependencies\NuGet\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
145+ <Private>True</Private>
146+ </Reference>
147+ <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL">
148+ <HintPath>..\..\Dependencies\NuGet\Owin.1.0\lib\net40\Owin.dll</HintPath>
149+ <Private>True</Private>
150+ </Reference>
151+ <Reference Include="RazorEngine, Version=3.7.7.0, Culture=neutral, PublicKeyToken=9ee697374c7e744a, processorArchitecture=MSIL">
152+ <HintPath>..\..\Dependencies\NuGet\RazorEngine.3.7.7\lib\net45\RazorEngine.dll</HintPath>
153+ <Private>True</Private>
154+ </Reference>
118155 <Reference Include="System" />
156+ <Reference Include="System.ComponentModel.DataAnnotations" />
119157 <Reference Include="System.configuration" />
120158 <Reference Include="System.Core">
121159 <RequiredTargetFramework>3.5</RequiredTargetFramework>
122160 </Reference>
161+ <Reference Include="System.IO" />
162+ <Reference Include="System.Net" />
163+ <Reference Include="System.Net.Http">
164+ <HintPath>..\..\Dependencies\NuGet\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.dll</HintPath>
165+ <Private>True</Private>
166+ </Reference>
167+ <Reference Include="System.Net.Http.Formatting, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
168+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll</HintPath>
169+ <Private>True</Private>
170+ </Reference>
171+ <Reference Include="System.Net.Http.WebRequest">
172+ <HintPath>..\..\Dependencies\NuGet\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
173+ <Private>True</Private>
174+ </Reference>
175+ <Reference Include="System.Security" />
123176 <Reference Include="System.ServiceModel" />
124177 <Reference Include="System.ServiceModel.Activation" />
125178 <Reference Include="System.ServiceModel.Web" />
126179 <Reference Include="System.Web" />
180+ <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
181+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll</HintPath>
182+ <Private>True</Private>
183+ </Reference>
184+ <Reference Include="System.Web.Http, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
185+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll</HintPath>
186+ <Private>True</Private>
187+ </Reference>
188+ <Reference Include="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
189+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll</HintPath>
190+ <Private>True</Private>
191+ </Reference>
192+ <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
193+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll</HintPath>
194+ <Private>True</Private>
195+ </Reference>
196+ <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
197+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll</HintPath>
198+ <Private>True</Private>
199+ </Reference>
200+ <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
201+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
202+ <Private>True</Private>
203+ </Reference>
204+ <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
205+ <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
206+ <Private>True</Private>
207+ </Reference>
127208 <Reference Include="System.Xml.Linq">
128209 <RequiredTargetFramework>3.5</RequiredTargetFramework>
129210 </Reference>
@@ -142,6 +223,8 @@ · 12 unmodified lines
142223 <Compile Include="Hosting\VirtualFileBaseCollection.cs" />
143224 <Compile Include="Embedded\ISecurityService.cs" />
144225 <Compile Include="IgnoreServicesSecurityModule.cs" />
226+ <Compile Include="Model\DataContext.cs" />
227+ <Compile Include="Model\RecordOperationsCache.cs" />
145228 <Compile Include="NamespaceDoc.cs" />
146229 <Compile Include="Properties\AssemblyInfo.cs" />
147230 <Compile Include="SecurityModule.cs" />
@@ -156,7 +239,14 @@ · 8 unmodified lines
156239 <DependentUpon>SecurityService.svc</DependentUpon>
157240 </Compile>
158241 <Compile Include="Embedded\SecurityServiceHostFactory.cs" />
242+ <Compile Include="Security\AuthorizationCache.cs" />
243+ <Compile Include="Security\AuthorizeControllerRoleAttribute.cs" />
244+ <Compile Include="Security\AuthorizeHubRoleAttribute.cs" />
245+ <Compile Include="Security\SecurityHub.cs" />
246+ <Compile Include="Templating\NamespaceDoc.cs" />
247+ <Compile Include="Templating\RazorView.cs" />
159248 <Compile Include="UI\ClientSideExtensions.cs" />
249+ <Compile Include="WebExtensions.cs" />
160250 </ItemGroup>
161251 <ItemGroup>
162252 <ProjectReference Include="..\GSF.Core\GSF.Core.csproj">
@@ -214,6 +304,21 @@ · 51 unmodified lines
214304 </ItemGroup>
215305 <ItemGroup>
216306 <None Include="GSF.Web.nuspec" />
307+ <EmbeddedResource Include="Model\Views\PagedViewModel.cshtml" />
308+ <None Include="packages.config" />
309+ <EmbeddedResource Include="Templating\Views\AddCheckBoxField.cshtml" />
310+ <EmbeddedResource Include="Templating\Views\AddDateField.cshtml" />
311+ <EmbeddedResource Include="Templating\Views\AddInputField.cshtml" />
312+ <EmbeddedResource Include="Templating\Views\AddSelectField.cshtml" />
313+ <EmbeddedResource Include="Templating\Views\AddTextAreaField.cshtml" />
314+ <EmbeddedResource Include="Security\Views\Groups.cshtml" />
315+ <EmbeddedResource Include="Security\Views\Users.cshtml" />
316+ </ItemGroup>
317+ <ItemGroup>
318+ <EmbeddedResource Include="Model\Scripts\pagedViewModel.js" />
319+ </ItemGroup>
320+ <ItemGroup>
321+ <EmbeddedResource Include="Model\Scripts\gsf.web.client.js" />
217322 </ItemGroup>
218323 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
219324 <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Source/Libraries/GSF.Web/Model/DataContext.cs+955−0
@@ -0,0 +1,955 @@
1+//******************************************************************************************************
2+// DataContext.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Generic;
26+using System.ComponentModel.DataAnnotations;
27+using System.Linq;
28+using System.Text;
29+using GSF.Collections;
30+using GSF.Data;
31+using GSF.Data.Model;
32+using GSF.Reflection;
33+using GSF.Web.Templating;
34+using RazorEngine.Templating;
35+
36+namespace GSF.Web.Model
37+{
38+ /// <summary>
39+ /// Defines a default data context object based on embedded resources.
40+ /// </summary>
41+ public class DataContext : DataContext<CSharpEmbeddedResource>
42+ {
43+ /// <summary>
44+ /// Creates a new <see cref="DataContext"/>.
45+ /// </summary>
46+ /// <param name="connection"><see cref="AdoDataConnection"/> to use; defaults to a new connection.</param>
47+ /// <param name="disposeConnection">Set to <c>true</c> to dispose the provided <paramref name="connection"/>.</param>
48+ /// <param name="exceptionHandler">Delegate to handle exceptions.</param>
49+ public DataContext(AdoDataConnection connection = null, bool disposeConnection = false, Action<Exception> exceptionHandler = null) :
50+ base(connection, disposeConnection, exceptionHandler)
51+ {
52+ }
53+
54+ /// <summary>
55+ /// Creates a new <see cref="DataContext"/> using the specified <paramref name="settingsCategory"/>.
56+ /// </summary>
57+ /// <param name="settingsCategory">Setting category that contains the connection settings.</param>
58+ /// <param name="exceptionHandler">Delegate to handle exceptions.</param>
59+ public DataContext(string settingsCategory, Action<Exception> exceptionHandler = null) :
60+ base(settingsCategory, exceptionHandler)
61+ {
62+ }
63+ }
64+
65+ /// <summary>
66+ /// Defines a data context for modeled tables.
67+ /// </summary>
68+ /// <typeparam name="TLanguage"><see cref="LanguageContraint"/> type to use for Razor views.</typeparam>
69+ public class DataContext<TLanguage> : IDisposable where TLanguage : LanguageContraint, new()
70+ {
71+ #region [ Members ]
72+
73+ // Fields
74+ private AdoDataConnection m_connection;
75+ private readonly Dictionary<Type, object> m_tableOperations;
76+ private readonly Action<Exception> m_exceptionHandler;
77+ private readonly Dictionary<string, Tuple<string, string>> m_fieldValidationParameters;
78+ private readonly List<Tuple<string, string>> m_fieldValueInitializers;
79+ private readonly List<string> m_definedDateFields;
80+ private readonly string m_settingsCategory;
81+ private readonly bool m_disposeConnection;
82+ private string m_initialFocusField;
83+ private string m_addDateFieldTemplate;
84+ private string m_addInputFieldTemplate;
85+ private string m_addTextAreaFieldTemplate;
86+ private string m_addSelectFieldTemplate;
87+ private string m_addCheckBoxFieldTemplate;
88+ private bool m_disposed;
89+
90+ #endregion
91+
92+ #region [ Constructors ]
93+
94+ /// <summary>
95+ /// Creates a new <see cref="DataContext{TLanguage}"/>.
96+ /// </summary>
97+ /// <param name="connection"><see cref="AdoDataConnection"/> to use; defaults to a new connection.</param>
98+ /// <param name="disposeConnection">Set to <c>true</c> to dispose the provided <paramref name="connection"/>.</param>
99+ /// <param name="exceptionHandler">Delegate to handle exceptions.</param>
100+ public DataContext(AdoDataConnection connection = null, bool disposeConnection = false, Action<Exception> exceptionHandler = null)
101+ {
102+ m_connection = connection;
103+ m_tableOperations = new Dictionary<Type, object>();
104+ m_exceptionHandler = exceptionHandler;
105+ m_fieldValidationParameters = new Dictionary<string, Tuple<string, string>>();
106+ m_fieldValueInitializers = new List<Tuple<string, string>>();
107+ m_definedDateFields = new List<string>();
108+ m_settingsCategory = "systemSettings";
109+ m_disposeConnection = disposeConnection || connection == null;
110+ }
111+
112+ /// <summary>
113+ /// Creates a new <see cref="DataContext{TLanguage}"/> using the specified <paramref name="settingsCategory"/>.
114+ /// </summary>
115+ /// <param name="settingsCategory">Setting category that contains the connection settings.</param>
116+ /// <param name="exceptionHandler">Delegate to handle exceptions.</param>
117+ public DataContext(string settingsCategory, Action<Exception> exceptionHandler = null)
118+ {
119+ m_tableOperations = new Dictionary<Type, object>();
120+ m_exceptionHandler = exceptionHandler;
121+ m_fieldValidationParameters = new Dictionary<string, Tuple<string, string>>();
122+ m_fieldValueInitializers = new List<Tuple<string, string>>();
123+ m_definedDateFields = new List<string>();
124+ m_settingsCategory = settingsCategory;
125+ m_disposeConnection = true;
126+ }
127+
128+ #endregion
129+
130+ #region [ Properties ]
131+
132+ /// <summary>
133+ /// Gets the <see cref="AdoDataConnection"/> for this <see cref="DataContext{TLanguage}"/>.
134+ /// </summary>
135+ public AdoDataConnection Connection => m_connection ?? (m_connection = new AdoDataConnection(m_settingsCategory));
136+
137+ /// <summary>
138+ /// Gets validation pattern and error message for rendered fields, if any.
139+ /// </summary>
140+ public Dictionary<string, Tuple<string, string>> FieldValidationParameters => m_fieldValidationParameters;
141+
142+ /// <summary>
143+ /// Gets field value initializers, if any.
144+ /// </summary>
145+ public List<Tuple<string, string>> FieldValueInitializers => m_fieldValueInitializers;
146+
147+ /// <summary>
148+ /// Gets defined date fields, if any.
149+ /// </summary>
150+ public List<string> DefinedDateFields => m_definedDateFields;
151+
152+ /// <summary>
153+ /// Gets field name designated for initial focus.
154+ /// </summary>
155+ public string InitialFocusField => m_initialFocusField;
156+
157+ /// <summary>
158+ /// Gets or sets the date field razor template file name.
159+ /// </summary>
160+ public string AddDateFieldTemplate
161+ {
162+ get
163+ {
164+ return m_addDateFieldTemplate ?? (m_addDateFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddDateField.cshtml");
165+ }
166+ set
167+ {
168+ m_addDateFieldTemplate = value;
169+ }
170+ }
171+
172+ /// <summary>
173+ /// Gets or sets the input field razor template file name.
174+ /// </summary>
175+ public string AddInputFieldTemplate
176+ {
177+ get
178+ {
179+ return m_addInputFieldTemplate ?? (m_addInputFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddInputField.cshtml");
180+ }
181+ set
182+ {
183+ m_addInputFieldTemplate = value;
184+ }
185+ }
186+
187+ /// <summary>
188+ /// Gets or sets the text area field razor template file name.
189+ /// </summary>
190+ public string AddTextAreaFieldTemplate
191+ {
192+ get
193+ {
194+ return m_addTextAreaFieldTemplate ?? (m_addTextAreaFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddTextAreaField.cshtml");
195+ }
196+ set
197+ {
198+ m_addTextAreaFieldTemplate = value;
199+ }
200+ }
201+
202+ /// <summary>
203+ /// Gets or sets the select field razor template file name.
204+ /// </summary>
205+ public string AddSelectFieldTemplate
206+ {
207+ get
208+ {
209+ return m_addSelectFieldTemplate ?? (m_addSelectFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddSelectField.cshtml");
210+ }
211+ set
212+ {
213+ m_addSelectFieldTemplate = value;
214+ }
215+ }
216+
217+ /// <summary>
218+ /// Gets or sets the check box field razor template file name.
219+ /// </summary>
220+ public string AddCheckBoxFieldTemplate
221+ {
222+ get
223+ {
224+ return m_addCheckBoxFieldTemplate ?? (m_addCheckBoxFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddCheckBoxField.cshtml");
225+ }
226+ set
227+ {
228+ m_addCheckBoxFieldTemplate = value;
229+ }
230+ }
231+
232+ #endregion
233+
234+ #region [ Methods ]
235+
236+ /// <summary>
237+ /// Releases all the resources used by the <see cref="DataContext{TLanguage}"/> object.
238+ /// </summary>
239+ public void Dispose()
240+ {
241+ Dispose(true);
242+ GC.SuppressFinalize(this);
243+ }
244+
245+ /// <summary>
246+ /// Releases the unmanaged resources used by the <see cref="DataContext{TLanguage}"/> object and optionally releases the managed resources.
247+ /// </summary>
248+ /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
249+ protected virtual void Dispose(bool disposing)
250+ {
251+ if (!m_disposed)
252+ {
253+ try
254+ {
255+ if (disposing)
256+ {
257+ if (m_disposeConnection)
258+ m_connection?.Dispose();
259+ }
260+ }
261+ finally
262+ {
263+ m_disposed = true; // Prevent duplicate dispose.
264+ }
265+ }
266+ }
267+
268+ /// <summary>
269+ /// Gets the table operations for the specified modeled table <typeparamref name="T"/>.
270+ /// </summary>
271+ /// <typeparam name="T">Modeled database table (or view).</typeparam>
272+ /// <returns>Table operations for the specified modeled table <typeparamref name="T"/>.</returns>
273+ public TableOperations<T> Table<T>() where T : class, new()
274+ {
275+ return m_tableOperations.GetOrAdd(typeof(T), type => new TableOperations<T>(Connection, m_exceptionHandler)) as TableOperations<T>;
276+ }
277+
278+ /// <summary>
279+ /// Gets the field name targeted as the primary label for the modeled table.
280+ /// </summary>
281+ /// <typeparam name="T">Modeled database table (or view).</typeparam>
282+ /// <returns>Field name targeted as the primary label for the modeled table.</returns>
283+ public string GetPrimaryLabelField<T>() where T : class, new()
284+ {
285+ PrimaryLabelAttribute primaryLabelAttribute;
286+
287+ if (typeof(T).TryGetAttribute(out primaryLabelAttribute) && !string.IsNullOrWhiteSpace(primaryLabelAttribute.FieldName))
288+ return primaryLabelAttribute.FieldName;
289+
290+ string[] fieldNames = Table<T>().GetFieldNames();
291+ return fieldNames.Length > 0 ? fieldNames[0] : "";
292+ }
293+
294+ /// <summary>
295+ /// Gets the field name targeted to mark a record as deleted.
296+ /// </summary>
297+ /// <typeparam name="T">Modeled database table (or view).</typeparam>
298+ /// <returns>Field name targeted to mark a record as deleted.</returns>
299+ public string GetIsDeletedFlag<T>() where T : class, new()
300+ {
301+ IsDeletedFlagAttribute isDeletedFlagAttribute;
302+
303+ if (typeof(T).TryGetAttribute(out isDeletedFlagAttribute) && !string.IsNullOrWhiteSpace(isDeletedFlagAttribute.FieldName))
304+ return isDeletedFlagAttribute.FieldName;
305+
306+ return null;
307+ }
308+
309+ /// <summary>
310+ /// Looks up proper user roles for paged based on modeled security in <see cref="RecordOperationsCache"/>.
311+ /// </summary>
312+ /// <typeparam name="T">Modeled database table (or view).</typeparam>
313+ /// <param name="cache">Data hub record operations cache.</param>
314+ /// <param name="viewBag">ViewBag for the current view.</param>
315+ /// <remarks>
316+ /// Typically used in paged view model scenarios and invoked by controller prior to view rendering.
317+ /// Security is controlled at hub level, so failure to call will not impact security but may result
318+ /// in screen enabling and/or showing controls that the user does not actually have access to and
319+ /// upon attempted use will result in a security error.
320+ /// </remarks>
321+ public void EstablishUserRolesForPage<T>(RecordOperationsCache cache, dynamic viewBag) where T : class, new()
322+ {
323+ // Get any authorized roles as defined in hub for key records operations of modeled table
324+ Tuple<string, string>[] recordOperations = cache.GetRecordOperations<T>();
325+
326+ // Create a function to check if a method exists for operation - if none is defined, read-only access will be assumed (e.g. for a view)
327+ Func<RecordOperation, string> getRoles = operationType =>
328+ {
329+ Tuple<string, string> recordOperation = recordOperations[(int)operationType];
330+ return string.IsNullOrEmpty(recordOperation?.Item1) ? "" : recordOperation.Item2 ?? "";
331+ };
332+
333+ viewBag.EditRoles = getRoles(RecordOperation.UpdateRecord);
334+ viewBag.AddNewRoles = getRoles(RecordOperation.AddNewRecord);
335+ viewBag.DeleteRoles = getRoles(RecordOperation.DeleteRecord);
336+ }
337+
338+ /// <summary>
339+ /// Renders client-side configuration script for paged view model.
340+ /// </summary>
341+ /// <typeparam name="T">Modeled database table (or view).</typeparam>
342+ /// <param name="cache">Data hub record operations cache.</param>
343+ /// <param name="viewBag">ViewBag for the view.</param>
344+ /// <param name="defaultSortField">Default sort field name, defaults to first primary key field. Prefix field name with a minus, i.e., '-', to default to descending sort.</param>
345+ /// <param name="parentKeys">Primary keys values of the parent record to load.</param>
346+ /// <returns>Rendered paged view model configuration script.</returns>
347+ public string RenderViewModelConfiguration<T>(RecordOperationsCache cache, dynamic viewBag, string defaultSortField = null, params object[] parentKeys) where T : class, new()
348+ {
349+ StringBuilder javascript = new StringBuilder();
350+ string[] primaryKeyFields = Table<T>().GetPrimaryKeyFieldNames();
351+ string defaultSortAscending = "true";
352+
353+ defaultSortField = defaultSortField ?? primaryKeyFields[0];
354+
355+ // Handle case for desired default descending sort order
356+ if (defaultSortField[0] == '-')
357+ {
358+ defaultSortField = defaultSortField.Substring(1);
359+ defaultSortAscending = "false";
360+ }
361+
362+ javascript.Append($@"// Configure view model
363+ viewModel.defaultSortField = ""{defaultSortField}"";
364+ viewModel.defaultSortAscending = {defaultSortAscending};
365+ viewModel.labelField = ""{GetPrimaryLabelField<T>()}"";
366+ viewModel.primaryKeyFields = [{primaryKeyFields.Select(fieldName => $"\"{fieldName}\"").ToDelimitedString(", ")}];
367+ ".FixForwardSpacing());
368+
369+ string showDeletedValue = null;
370+
371+ if (viewBag.IsDeletedField != null)
372+ showDeletedValue = (viewBag.ShowDeleted ?? false).ToString().ToLower();
373+
374+ Func<string, string> toCamelCase = methodName => methodName == null ? null : $"{char.ToLower(methodName[0])}{methodName.Substring(1)}";
375+
376+ // Get method names for records operations of modeled table
377+ Tuple<string, string>[] recordOperations = cache.GetRecordOperations<T>();
378+ string queryRecordCountMethod = toCamelCase(recordOperations[(int)RecordOperation.QueryRecordCount]?.Item1);
379+ string queryRecordsMethod = toCamelCase(recordOperations[(int)RecordOperation.QueryRecords]?.Item1);
380+ string deleteRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.DeleteRecord]?.Item1);
381+ string createNewRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.CreateNewRecord]?.Item1);
382+ string addNewRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.AddNewRecord]?.Item1);
383+ string updateMethod = toCamelCase(recordOperations[(int)RecordOperation.UpdateRecord]?.Item1);
384+
385+ string keyValues = null;
386+
387+ if (parentKeys.Length > 0)
388+ keyValues = parentKeys.ToDelimitedString(", ");
389+
390+ // If modeled table has IsDeletedField marker, the showDeleted parameter should come first in DataHub operations
391+ if (showDeletedValue != null)
392+ keyValues = keyValues != null ? $"{showDeletedValue}, {keyValues}" : showDeletedValue;
393+
394+ if (!string.IsNullOrWhiteSpace(queryRecordCountMethod))
395+ javascript.Append($@"
396+ viewModel.setQueryRecordCount(function () {{
397+ return dataHub.{queryRecordCountMethod}({keyValues});
398+ }});
399+ ".FixForwardSpacing());
400+
401+ if (!string.IsNullOrWhiteSpace(queryRecordsMethod))
402+ javascript.Append($@"
403+ viewModel.setQueryRecords(function (sortField, ascending, page, pageSize) {{
404+ return dataHub.{queryRecordsMethod}({(keyValues == null ? "" : $"{keyValues}, ")}sortField, ascending, page, pageSize);
405+ }});
406+ ".FixForwardSpacing());
407+
408+ if (!string.IsNullOrWhiteSpace(deleteRecordMethod))
409+ javascript.Append($@"
410+ viewModel.setDeleteRecord(function (keyValues) {{
411+ return dataHub.{deleteRecordMethod}({Enumerable.Range(0, Table<T>().GetPrimaryKeyFieldNames().Length).Select(index => $"keyValues[{index}]").ToDelimitedString(", ")});
412+ }});
413+ ".FixForwardSpacing());
414+
415+ if (!string.IsNullOrWhiteSpace(createNewRecordMethod))
416+ javascript.Append($@"
417+ viewModel.setNewRecord(function () {{
418+ return dataHub.{createNewRecordMethod}();
419+ }});
420+ ".FixForwardSpacing());
421+
422+ if (!string.IsNullOrWhiteSpace(addNewRecordMethod))
423+ javascript.Append($@"
424+ viewModel.setAddNewRecord(function (record) {{
425+ return dataHub.{addNewRecordMethod}(record);
426+ }});
427+ ".FixForwardSpacing());
428+
429+ if (!string.IsNullOrWhiteSpace(updateMethod))
430+ javascript.Append($@"
431+ viewModel.setUpdateRecord(function (record) {{
432+ return dataHub.{updateMethod}(record);
433+ }});
434+ ".FixForwardSpacing());
435+
436+ return javascript.ToString();
437+ }
438+
439+ /// <summary>
440+ /// Adds a new pattern based validation and option error message to a field.
441+ /// </summary>
442+ /// <param name="observableFieldReference">Observable field reference (from JS view model).</param>
443+ /// <param name="validationPattern">Regex based validation pattern.</param>
444+ /// <param name="errorMessage">Optional error message to display when pattern fails.</param>
445+ public void AddFieldValidation(string observableFieldReference, string validationPattern, string errorMessage = null)
446+ {
447+ m_fieldValidationParameters[observableFieldReference] = new Tuple<string, string>(validationPattern, errorMessage);
448+ }
449+
450+ /// <summary>
451+ /// Adds a new field value initializer.
452+ /// </summary>
453+ /// <param name="fieldName">Field name (as defined in model).</param>
454+ /// <param name="initialValue">Javascript based initial value for field.</param>
455+ public void AddFieldValueInitializer(string fieldName, string initialValue = null)
456+ {
457+ m_fieldValueInitializers.Add(new Tuple<string, string>(fieldName, initialValue ?? "\"\""));
458+ }
459+
460+ /// <summary>
461+ /// Adds a new field value initializer based on modeled table field attributes.
462+ /// </summary>
463+ /// <param name="fieldName">Field name (as defined in model).</param>
464+ public void AddFieldValueInitializer<T>(string fieldName) where T : class, new()
465+ {
466+ InitialValueAttribute initialValueAttribute;
467+
468+ if (Table<T>().TryGetFieldAttribute(fieldName, out initialValueAttribute))
469+ AddFieldValueInitializer(fieldName, initialValueAttribute.InitialValue);
470+ }
471+
472+ /// <summary>
473+ /// Generates template based input date field based on reflected modeled table field attributes.
474+ /// </summary>
475+ /// <typeparam name="T">Modeled table.</typeparam>
476+ /// <param name="fieldName">Field name for input date field.</param>
477+ /// <param name="inputType">Input field type, defaults to text.</param>
478+ /// <param name="fieldLabel">Label name for input date field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param>
479+ /// <param name="fieldID">ID to use for input field; defaults to date + <paramref name="fieldName"/>.</param>
480+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
481+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
482+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
483+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
484+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
485+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
486+ /// <param name="initialFocus">Use field for initial focus.</param>
487+ /// <returns>Generated HTML for new date based input field based on modeled table field attributes.</returns>
488+ public string AddDateField<T>(string fieldName, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new()
489+ {
490+ TableOperations<T> tableOperations = Table<T>();
491+ StringLengthAttribute stringLengthAttribute;
492+ RegularExpressionAttribute regularExpressionAttribute;
493+
494+ if (string.IsNullOrEmpty(fieldLabel))
495+ {
496+ LabelAttribute labelAttribute;
497+
498+ if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute))
499+ fieldLabel = labelAttribute.Label;
500+ }
501+
502+ tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute);
503+ tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute);
504+
505+ if ((object)regularExpressionAttribute != null)
506+ {
507+ string observableReference;
508+
509+ if (string.IsNullOrEmpty(groupDataBinding))
510+ observableReference = $"viewModel.currentRecord().{fieldName}";
511+ else // "with: $root.connectionString"
512+ observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}";
513+
514+ AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format.");
515+ }
516+
517+ AddFieldValueInitializer<T>(fieldName);
518+
519+ return AddDateField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName),
520+ stringLengthAttribute?.MaximumLength ?? 0, inputType, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus);
521+ }
522+
523+ /// <summary>
524+ /// Generates template based input date field based on specified parameters.
525+ /// </summary>
526+ /// <param name="fieldName">Field name for input date field.</param>
527+ /// <param name="required">Determines if field name is required.</param>
528+ /// <param name="maxLength">Defines maximum input field length.</param>
529+ /// <param name="inputType">Input field type, defaults to text.</param>
530+ /// <param name="fieldLabel">Label name for input date field, defaults to <paramref name="fieldName"/>.</param>
531+ /// <param name="fieldID">ID to use for input field; defaults to date + <paramref name="fieldName"/>.</param>
532+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
533+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
534+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
535+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
536+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
537+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
538+ /// <param name="initialFocus">Use field for initial focus.</param>
539+ /// <returns>Generated HTML for new date based input field based on specified parameters.</returns>
540+ public string AddDateField(string fieldName, bool required, int maxLength = 0, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false)
541+ {
542+ RazorView<TLanguage> addDateFieldTemplate = new RazorView<TLanguage>(AddDateFieldTemplate, m_exceptionHandler);
543+ DynamicViewBag viewBag = addDateFieldTemplate.ViewBag;
544+
545+ if (string.IsNullOrEmpty(fieldID))
546+ fieldID = $"date{fieldName}";
547+
548+ if (initialFocus)
549+ m_initialFocusField = fieldID;
550+
551+ viewBag.AddValue("FieldName", fieldName);
552+ viewBag.AddValue("Required", required);
553+ viewBag.AddValue("MaxLength", maxLength);
554+ viewBag.AddValue("InputType", inputType ?? "text");
555+ viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName);
556+ viewBag.AddValue("FieldID", fieldID);
557+ viewBag.AddValue("GroupDataBinding", groupDataBinding);
558+ viewBag.AddValue("LabelDataBinding", labelDataBinding);
559+ viewBag.AddValue("RequiredDataBinding", requiredDataBinding);
560+ viewBag.AddValue("CustomDataBinding", customDataBinding);
561+ viewBag.AddValue("DependencyFieldName", dependencyFieldName);
562+ viewBag.AddValue("ToolTip", toolTip);
563+
564+ m_definedDateFields.Add(fieldName);
565+
566+ return addDateFieldTemplate.Execute();
567+ }
568+
569+ /// <summary>
570+ /// Generates template based input text field based on reflected modeled table field attributes.
571+ /// </summary>
572+ /// <typeparam name="T">Modeled table.</typeparam>
573+ /// <param name="fieldName">Field name for input text field.</param>
574+ /// <param name="inputType">Input field type, defaults to text.</param>
575+ /// <param name="fieldLabel">Label name for input text field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param>
576+ /// <param name="fieldID">ID to use for input field; defaults to input + <paramref name="fieldName"/>.</param>
577+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
578+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
579+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
580+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
581+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
582+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
583+ /// <param name="initialFocus">Use field for initial focus.</param>
584+ /// <returns>Generated HTML for new input field based on modeled table field attributes.</returns>
585+ public string AddInputField<T>(string fieldName, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new()
586+ {
587+ TableOperations<T> tableOperations = Table<T>();
588+ StringLengthAttribute stringLengthAttribute;
589+ RegularExpressionAttribute regularExpressionAttribute;
590+
591+ if (string.IsNullOrEmpty(inputType))
592+ {
593+ Type fieldType = tableOperations.GetFieldType(fieldName);
594+
595+ if (IsNumericType(fieldType))
596+ inputType = "number";
597+ else if (fieldType == typeof(DateTime))
598+ inputType = "date";
599+ }
600+
601+ if (string.IsNullOrEmpty(fieldLabel))
602+ {
603+ LabelAttribute labelAttribute;
604+
605+ if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute))
606+ fieldLabel = labelAttribute.Label;
607+ }
608+
609+ tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute);
610+ tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute);
611+
612+ if ((object)regularExpressionAttribute != null)
613+ {
614+ string observableReference;
615+
616+ if (string.IsNullOrEmpty(groupDataBinding))
617+ observableReference = $"viewModel.currentRecord().{fieldName}";
618+ else // "with: $root.connectionString"
619+ observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}";
620+
621+ AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format.");
622+ }
623+
624+ AddFieldValueInitializer<T>(fieldName);
625+
626+ return AddInputField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName),
627+ stringLengthAttribute?.MaximumLength ?? 0, inputType, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus);
628+ }
629+
630+ /// <summary>
631+ /// Generates template based input text field based on specified parameters.
632+ /// </summary>
633+ /// <param name="fieldName">Field name for input text field.</param>
634+ /// <param name="required">Determines if field name is required.</param>
635+ /// <param name="maxLength">Defines maximum input field length.</param>
636+ /// <param name="inputType">Input field type, defaults to text.</param>
637+ /// <param name="fieldLabel">Label name for input text field, defaults to <paramref name="fieldName"/>.</param>
638+ /// <param name="fieldID">ID to use for input field; defaults to input + <paramref name="fieldName"/>.</param>
639+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
640+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
641+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
642+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
643+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
644+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
645+ /// <param name="initialFocus">Use field for initial focus.</param>
646+ /// <returns>Generated HTML for new input field based on specified parameters.</returns>
647+ public string AddInputField(string fieldName, bool required, int maxLength = 0, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false)
648+ {
649+ RazorView<TLanguage> addInputFieldTemplate = new RazorView<TLanguage>(AddInputFieldTemplate, m_exceptionHandler);
650+ DynamicViewBag viewBag = addInputFieldTemplate.ViewBag;
651+
652+ if (string.IsNullOrEmpty(fieldID))
653+ fieldID = $"input{fieldName}";
654+
655+ if (initialFocus)
656+ m_initialFocusField = fieldID;
657+
658+ viewBag.AddValue("FieldName", fieldName);
659+ viewBag.AddValue("Required", required);
660+ viewBag.AddValue("MaxLength", maxLength);
661+ viewBag.AddValue("InputType", inputType ?? "text");
662+ viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName);
663+ viewBag.AddValue("FieldID", fieldID);
664+ viewBag.AddValue("GroupDataBinding", groupDataBinding);
665+ viewBag.AddValue("LabelDataBinding", labelDataBinding);
666+ viewBag.AddValue("RequiredDataBinding", requiredDataBinding);
667+ viewBag.AddValue("CustomDataBinding", customDataBinding);
668+ viewBag.AddValue("DependencyFieldName", dependencyFieldName);
669+ viewBag.AddValue("ToolTip", toolTip);
670+
671+ return addInputFieldTemplate.Execute();
672+ }
673+
674+ /// <summary>
675+ /// Generates template based text area field based on reflected modeled table field attributes.
676+ /// </summary>
677+ /// <typeparam name="T">Modeled table.</typeparam>
678+ /// <param name="fieldName">Field name for text area field.</param>
679+ /// <param name="rows">Number of rows for text area.</param>
680+ /// <param name="fieldLabel">Label name for text area field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param>
681+ /// <param name="fieldID">ID to use for text area field; defaults to text + <paramref name="fieldName"/>.</param>
682+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
683+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
684+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
685+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
686+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
687+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
688+ /// <param name="initialFocus">Use field for initial focus.</param>
689+ /// <returns>Generated HTML for new text area field based on modeled table field attributes.</returns>
690+ public string AddTextAreaField<T>(string fieldName, int rows = 2, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new()
691+ {
692+ TableOperations<T> tableOperations = Table<T>();
693+ StringLengthAttribute stringLengthAttribute;
694+ RegularExpressionAttribute regularExpressionAttribute;
695+
696+ if (string.IsNullOrEmpty(fieldLabel))
697+ {
698+ LabelAttribute labelAttribute;
699+
700+ if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute))
701+ fieldLabel = labelAttribute.Label;
702+ }
703+
704+ tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute);
705+ tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute);
706+
707+ if ((object)regularExpressionAttribute != null)
708+ {
709+ string observableReference;
710+
711+ if (string.IsNullOrEmpty(groupDataBinding))
712+ observableReference = $"viewModel.currentRecord().{fieldName}";
713+ else // "with: $root.connectionString"
714+ observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}";
715+
716+ AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format.");
717+ }
718+
719+ AddFieldValueInitializer<T>(fieldName);
720+
721+ return AddTextAreaField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName),
722+ stringLengthAttribute?.MaximumLength ?? 0, rows, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus);
723+ }
724+
725+ /// <summary>
726+ /// Generates template based text area field based on specified parameters.
727+ /// </summary>
728+ /// <param name="fieldName">Field name for text area field.</param>
729+ /// <param name="required">Determines if field name is required.</param>
730+ /// <param name="maxLength">Defines maximum text area field length.</param>
731+ /// <param name="rows">Number of rows for text area.</param>
732+ /// <param name="fieldLabel">Label name for text area field, defaults to <paramref name="fieldName"/>.</param>
733+ /// <param name="fieldID">ID to use for text area field; defaults to text + <paramref name="fieldName"/>.</param>
734+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
735+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
736+ /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param>
737+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
738+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
739+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
740+ /// <param name="initialFocus">Use field for initial focus.</param>
741+ /// <returns>Generated HTML for new text area field based on specified parameters.</returns>
742+ public string AddTextAreaField(string fieldName, bool required, int maxLength = 0, int rows = 2, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false)
743+ {
744+ RazorView<TLanguage> addTextAreaTemplate = new RazorView<TLanguage>(AddTextAreaFieldTemplate, m_exceptionHandler);
745+ DynamicViewBag viewBag = addTextAreaTemplate.ViewBag;
746+
747+ if (string.IsNullOrEmpty(fieldID))
748+ fieldID = $"text{fieldName}";
749+
750+ if (initialFocus)
751+ m_initialFocusField = fieldID;
752+
753+ viewBag.AddValue("FieldName", fieldName);
754+ viewBag.AddValue("Required", required);
755+ viewBag.AddValue("MaxLength", maxLength);
756+ viewBag.AddValue("Rows", rows);
757+ viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName);
758+ viewBag.AddValue("FieldID", fieldID);
759+ viewBag.AddValue("GroupDataBinding", groupDataBinding);
760+ viewBag.AddValue("LabelDataBinding", labelDataBinding);
761+ viewBag.AddValue("RequiredDataBinding", requiredDataBinding);
762+ viewBag.AddValue("CustomDataBinding", customDataBinding);
763+ viewBag.AddValue("DependencyFieldName", dependencyFieldName);
764+ viewBag.AddValue("ToolTip", toolTip);
765+
766+ return addTextAreaTemplate.Execute();
767+ }
768+
769+ /// <summary>
770+ /// Generates template based select field based on reflected modeled table field attributes.
771+ /// </summary>
772+ /// <typeparam name="TSelect">Modeled table for select field.</typeparam>
773+ /// <typeparam name="TOption">Modeled table for option data.</typeparam>
774+ /// <param name="fieldName">Field name for value of select field.</param>
775+ /// <param name="optionValueFieldName">Field name for ID of option data.</param>
776+ /// <param name="optionLabelFieldName">Field name for label of option data, defaults to <paramref name="optionValueFieldName"/></param>
777+ /// <param name="optionSortFieldName">Field name for sort order of option data, defaults to <paramref name="optionLabelFieldName"/></param>
778+ /// <param name="fieldLabel">Label name for select field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param>
779+ /// <param name="fieldID">ID to use for select field; defaults to select + <paramref name="fieldName"/>.</param>
780+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
781+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
782+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
783+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
784+ /// <param name="optionDataBinding">Data-bind operations to apply to each option value, if any.</param>
785+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
786+ /// <param name="initialFocus">Use field for initial focus.</param>
787+ /// <param name="restriction">Record restriction to apply, if any.</param>
788+ /// <returns>Generated HTML for new text field based on modeled table field attributes.</returns>
789+ public string AddSelectField<TSelect, TOption>(string fieldName, string optionValueFieldName, string optionLabelFieldName = null, string optionSortFieldName = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string optionDataBinding = null, string toolTip = null, bool initialFocus = false, RecordRestriction restriction = null) where TSelect : class, new() where TOption : class, new()
790+ {
791+ if (string.IsNullOrEmpty(fieldLabel))
792+ {
793+ LabelAttribute labelAttribute;
794+
795+ if (Table<TSelect>().TryGetFieldAttribute(fieldName, out labelAttribute))
796+ fieldLabel = labelAttribute.Label;
797+ }
798+
799+ AddFieldValueInitializer<TSelect>(fieldName);
800+
801+ return AddSelectField<TOption>(fieldName, Table<TSelect>().FieldHasAttribute<RequiredAttribute>(fieldName),
802+ optionValueFieldName, optionLabelFieldName, optionSortFieldName, fieldLabel, fieldID, groupDataBinding, labelDataBinding, customDataBinding, dependencyFieldName, optionDataBinding, toolTip, initialFocus, restriction);
803+ }
804+
805+ /// <summary>
806+ /// Generates template based select field based on specified parameters.
807+ /// </summary>
808+ /// <typeparam name="TOption">Modeled table for option data.</typeparam>
809+ /// <param name="fieldName">Field name for value of select field.</param>
810+ /// <param name="required">Determines if field name is required.</param>
811+ /// <param name="optionValueFieldName">Field name for ID of option data.</param>
812+ /// <param name="optionLabelFieldName">Field name for label of option data, defaults to <paramref name="optionValueFieldName"/></param>
813+ /// <param name="optionSortFieldName">Field name for sort order of option data, defaults to <paramref name="optionLabelFieldName"/></param>
814+ /// <param name="fieldLabel">Label name for select field, defaults to <paramref name="fieldName"/>.</param>
815+ /// <param name="fieldID">ID to use for select field; defaults to select + <paramref name="fieldName"/>.</param>
816+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
817+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
818+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
819+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
820+ /// <param name="optionDataBinding">Data-bind operations to apply to each option value, if any.</param>
821+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
822+ /// <param name="initialFocus">Use field for initial focus.</param>
823+ /// <param name="restriction">Record restriction to apply, if any.</param>
824+ /// <returns>Generated HTML for new text field based on specified parameters.</returns>
825+ public string AddSelectField<TOption>(string fieldName, bool required, string optionValueFieldName, string optionLabelFieldName = null, string optionSortFieldName = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string optionDataBinding = null, string toolTip = null, bool initialFocus = false, RecordRestriction restriction = null) where TOption : class, new()
826+ {
827+ RazorView<TLanguage> addSelectFieldTemplate = new RazorView<TLanguage>(AddSelectFieldTemplate, m_exceptionHandler);
828+ DynamicViewBag viewBag = addSelectFieldTemplate.ViewBag;
829+ TableOperations<TOption> optionTableOperations = Table<TOption>();
830+ Dictionary<string, string> options = new Dictionary<string, string>();
831+ string optionTableName = typeof(TOption).Name;
832+
833+ optionLabelFieldName = optionLabelFieldName ?? optionValueFieldName;
834+ optionSortFieldName = optionSortFieldName ?? optionLabelFieldName;
835+ fieldLabel = fieldLabel ?? optionTableName;
836+
837+ if (string.IsNullOrEmpty(fieldID))
838+ fieldID = $"select{fieldName}";
839+
840+ if (initialFocus)
841+ m_initialFocusField = fieldID;
842+
843+ viewBag.AddValue("FieldName", fieldName);
844+ viewBag.AddValue("Required", required);
845+ viewBag.AddValue("FieldLabel", fieldLabel);
846+ viewBag.AddValue("FieldID", fieldID);
847+ viewBag.AddValue("GroupDataBinding", groupDataBinding);
848+ viewBag.AddValue("LabelDataBinding", labelDataBinding);
849+ viewBag.AddValue("CustomDataBinding", customDataBinding);
850+ viewBag.AddValue("DependencyFieldName", dependencyFieldName);
851+ viewBag.AddValue("ToolTip", toolTip);
852+ viewBag.AddValue("OptionDataBinding", optionDataBinding);
853+
854+ foreach (TOption record in Table<TOption>().QueryRecords(optionSortFieldName, restriction))
855+ {
856+ if (record != null)
857+ options.Add(optionTableOperations.GetFieldValue(record, optionValueFieldName).ToString(), optionTableOperations.GetFieldValue(record, optionLabelFieldName).ToNonNullString(fieldLabel));
858+ }
859+
860+ viewBag.AddValue("Options", options);
861+
862+ return addSelectFieldTemplate.Execute();
863+ }
864+
865+ /// <summary>
866+ /// Generates template based check box field based on reflected modeled table field attributes.
867+ /// </summary>
868+ /// <typeparam name="T">Modeled table.</typeparam>
869+ /// <param name="fieldName">Field name for value of check box field.</param>
870+ /// <param name="fieldLabel">Label name for check box field, defaults to <paramref name="fieldName"/>.</param>
871+ /// <param name="fieldID">ID to use for check box field; defaults to check + <paramref name="fieldName"/>.</param>
872+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
873+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
874+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
875+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
876+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
877+ /// <param name="initialFocus">Use field for initial focus.</param>
878+ /// <returns>Generated HTML for new check box field based on modeled table field attributes.</returns>
879+ public string AddCheckBoxField<T>(string fieldName, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new()
880+ {
881+ if (string.IsNullOrEmpty(fieldLabel))
882+ {
883+ LabelAttribute labelAttribute;
884+
885+ if (Table<T>().TryGetFieldAttribute(fieldName, out labelAttribute))
886+ fieldLabel = labelAttribute.Label;
887+ }
888+
889+ AddFieldValueInitializer<T>(fieldName);
890+
891+ return AddCheckBoxField(fieldName, fieldLabel, fieldID, groupDataBinding, labelDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus);
892+ }
893+
894+ /// <summary>
895+ /// Generates template based check box field based on specified parameters.
896+ /// </summary>
897+ /// <param name="fieldName">Field name for value of check box field.</param>
898+ /// <param name="fieldLabel">Label name for check box field, defaults to <paramref name="fieldName"/>.</param>
899+ /// <param name="fieldID">ID to use for check box field; defaults to check + <paramref name="fieldName"/>.</param>
900+ /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param>
901+ /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param>
902+ /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param>
903+ /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param>
904+ /// <param name="toolTip">Tool tip text to apply to field, if any.</param>
905+ /// <param name="initialFocus">Use field for initial focus.</param>
906+ /// <returns>Generated HTML for new check box field based on specified parameters.</returns>
907+ public string AddCheckBoxField(string fieldName, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false)
908+ {
909+ RazorView<TLanguage> addCheckBoxFieldTemplate = new RazorView<TLanguage>(AddCheckBoxFieldTemplate, m_exceptionHandler);
910+ DynamicViewBag viewBag = addCheckBoxFieldTemplate.ViewBag;
911+
912+ if (string.IsNullOrEmpty(fieldID))
913+ fieldID = $"check{fieldName}";
914+
915+ if (initialFocus)
916+ m_initialFocusField = fieldID;
917+
918+ viewBag.AddValue("FieldName", fieldName);
919+ viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName);
920+ viewBag.AddValue("FieldID", fieldID);
921+ viewBag.AddValue("GroupDataBinding", groupDataBinding);
922+ viewBag.AddValue("LabelDataBinding", labelDataBinding);
923+ viewBag.AddValue("CustomDataBinding", customDataBinding);
924+ viewBag.AddValue("DependencyFieldName", dependencyFieldName);
925+ viewBag.AddValue("ToolTip", toolTip);
926+
927+ return addCheckBoxFieldTemplate.Execute();
928+ }
929+
930+ #endregion
931+
932+ #region [ Static ]
933+
934+ // Static Methods
935+ private static bool IsNumericType(Type type)
936+ {
937+ return
938+ type == typeof(byte) ||
939+ type == typeof(sbyte) ||
940+ type == typeof(short) ||
941+ type == typeof(ushort) ||
942+ type == typeof(Int24) ||
943+ type == typeof(UInt24) ||
944+ type == typeof(int) ||
945+ type == typeof(uint) ||
946+ type == typeof(long) ||
947+ type == typeof(ulong) ||
948+ type == typeof(float) ||
949+ type == typeof(double) ||
950+ type == typeof(decimal);
951+ }
952+
953+ #endregion
954+ }
955+}
Source/Libraries/GSF.Web/Model/RecordOperationsCache.cs+91−0
@@ -0,0 +1,91 @@
1+//******************************************************************************************************
2+// RecordOperationsCache.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/14/2016 - Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Generic;
26+using System.Reflection;
27+using GSF.Collections;
28+using GSF.Data.Model;
29+using GSF.Reflection;
30+using GSF.Web.Security;
31+
32+namespace GSF.Web.Model
33+{
34+ /// <summary>
35+ /// Defines class that caches data hub operations.
36+ /// </summary>
37+ public class RecordOperationsCache
38+ {
39+ #region [ Members ]
40+
41+ // Fields
42+ private readonly Dictionary<Type, Tuple<string, string>[]> m_recordOperations;
43+
44+ #endregion
45+
46+ #region [ Constructors ]
47+
48+ /// <summary>
49+ /// Creates a new <see cref="RecordOperationsCache"/>.
50+ /// </summary>
51+ /// <param name="hub">Hub to analyze for record operations.</param>
52+ public RecordOperationsCache(Type hub)
53+ {
54+ int recordOperations = Enum.GetValues(typeof(RecordOperation)).Length;
55+ m_recordOperations = new Dictionary<Type, Tuple<string, string>[]>();
56+
57+ // Analyze and cache data hub methods that are targeted for record operations
58+ foreach (MethodInfo method in hub.GetMethods(BindingFlags.Public | BindingFlags.Instance))
59+ {
60+ AuthorizeHubRoleAttribute authorizeHubRoleAttribute;
61+ RecordOperationAttribute recordOperationAttribute;
62+
63+ method.TryGetAttribute(out authorizeHubRoleAttribute);
64+
65+ if (method.TryGetAttribute(out recordOperationAttribute))
66+ {
67+ // Cache method name and any defined authorized roles for current record operation
68+ m_recordOperations.GetOrAdd(recordOperationAttribute.ModelType,
69+ type => new Tuple<string, string>[recordOperations])[(int)recordOperationAttribute.Operation] =
70+ new Tuple<string, string>(method.Name, authorizeHubRoleAttribute?.Roles);
71+ }
72+ }
73+ }
74+
75+ #endregion
76+
77+ #region [ Methods ]
78+
79+ /// <summary>
80+ /// Gets record operation methods for specified modeled table.
81+ /// </summary>
82+ /// <typeparam name="T">Modeled table.</typeparam>
83+ /// <returns>record operation methods for specified modeled table.</returns>
84+ public Tuple<string, string>[] GetRecordOperations<T>() where T : class, new()
85+ {
86+ return m_recordOperations[typeof(T)];
87+ }
88+
89+ #endregion
90+ }
91+}
Source/Libraries/GSF.Web/Model/Scripts/gsf.web.client.js+667−0
@@ -0,0 +1,667 @@
1+//******************************************************************************************************
2+// gsf.web.client.js - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/26/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+// ReSharper disable NativeTypePrototypeExtending
24+
25+// Grid Solutions Framework Core Web Client Script Functions
26+"use strict";
27+
28+const isIE = detectIE();
29+var textMetrics;
30+
31+$(function () {
32+ // Create a canvas object that will be used for text metrics calculations
33+ $("<canvas id=\"textMetricsCanvas\" height=\"1px\" width=\"1px\" style=\"visibility: hidden\"></canvas>").appendTo("body");
34+
35+ // Get text metrics canvas context
36+ textMetrics = document.getElementById("textMetricsCanvas").getContext("2d");
37+});
38+
39+// Miscellaneous functions
40+function notNull(value, nonNullValue) {
41+ return value || (nonNullValue || "");
42+}
43+
44+function detectIE() {
45+ const ua = window.navigator.userAgent;
46+ const msie = ua.indexOf("MSIE ");
47+
48+ if (msie > 0) {
49+ // IE 10 or older => return version number
50+ return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)), 10);
51+ }
52+
53+ const trident = ua.indexOf("Trident/");
54+
55+ if (trident > 0) {
56+ // IE 11 => return version number
57+ const rv = ua.indexOf("rv:");
58+ return parseInt(ua.substring(rv + 3, ua.indexOf(".", rv)), 10);
59+ }
60+
61+ const edge = ua.indexOf("Edge/");
62+
63+ if (edge > 0) {
64+ // Edge (IE 12+) => return version number
65+ return parseInt(ua.substring(edge + 5, ua.indexOf(".", edge)), 10);
66+ }
67+
68+ // Other browser
69+ return false;
70+}
71+
72+function isNumber(val) {
73+ return !isNaN(parseFloat(val)) && isFinite(val);
74+}
75+
76+function isBool(val) {
77+ if (typeof value === "boolean")
78+ return true;
79+
80+ const lval = val.toString().toLowerCase();
81+ return lval === "true" || lval === "false";
82+}
83+
84+function getBool(val) {
85+ const num = +val;
86+ return !isNaN(num) ? !!num : !!String(val).toLowerCase().replace(false, "");
87+}
88+
89+// Number functions
90+Number.prototype.truncate = function() {
91+ if (typeof Math.trunc != "function")
92+ return parseInt(this.toString());
93+
94+ return Math.trunc(this);
95+}
96+
97+Number.prototype.padLeft = function (totalWidth, paddingChar) {
98+ return this.truncate().toString().padLeft(totalWidth, paddingChar || "0");
99+}
100+
101+Number.prototype.padRight = function (totalWidth, paddingChar) {
102+ return this.truncate().toString().padRight(totalWidth, paddingChar || "0");
103+}
104+
105+// Array functions
106+
107+// Combines a dictionary of key-value pairs into a string. Values will be escaped within startValueDelimiter and endValueDelimiter
108+// to contain nested key/value pair expressions like the following: "normalKVP=-1; nestedKVP={p1=true; p2=0.001}" when either the
109+// parameterDelimiter or the keyValueDelimiter are detected in the value of the key/value pair.
110+function joinKeyValuePairs (source, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) {
111+ if (!parameterDelimiter)
112+ parameterDelimiter = ";";
113+
114+ if (!keyValueDelimiter)
115+ keyValueDelimiter = "=";
116+
117+ if (!startValueDelimiter)
118+ startValueDelimiter = "{";
119+
120+ if (!endValueDelimiter)
121+ endValueDelimiter = "}";
122+
123+ const values = [];
124+
125+ for (var key in source) {
126+ if (source.hasOwnProperty(key)) {
127+ var value = source[key];
128+
129+ if (isBool(value))
130+ value = value.toString().toLowerCase();
131+ else
132+ value = value ? value.toString() : "";
133+
134+ if (value.indexOf(parameterDelimiter) >= 0 || value.indexOf(keyValueDelimiter) >= 0)
135+ value = startValueDelimiter + value + endValueDelimiter;
136+
137+ values.push(key + keyValueDelimiter + value);
138+ }
139+ }
140+
141+ return values.join(parameterDelimiter + " ");
142+};
143+
144+Array.prototype.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) {
145+ return joinKeyValuePairs(this, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter);
146+};
147+
148+// Represents a dictionary style class with case-insensitive keys
149+function Dictionary(source) {
150+ const self = this;
151+
152+ if (!source)
153+ source = [];
154+
155+ self._keys = [];
156+ self._values = [];
157+
158+ self.count = function () {
159+ var size = 0;
160+
161+ for (var property in self._values) {
162+ if (self._values.hasOwnProperty(property))
163+ size++;
164+ }
165+
166+ return size;
167+ }
168+
169+ self.keys = function () {
170+ const keys = [];
171+
172+ for (var property in self._keys) {
173+ if (self._keys.hasOwnProperty(property))
174+ keys.push(self._keys[property]);
175+ }
176+
177+ return keys;
178+ }
179+
180+ self.values = function () {
181+ const values = [];
182+
183+ for (var property in self._values) {
184+ if (self._keys.hasOwnProperty(property))
185+ values.push(self._values[property]);
186+ }
187+
188+ return values;
189+ }
190+
191+ self.get = function (key) {
192+ return self._values[String(key).toLowerCase()];
193+ }
194+
195+ self.set = function (key, value) {
196+ const lkey = String(key).toLowerCase();
197+
198+ if (!self._keys[lkey] || self._keys[lkey].toLowerCase() !== key)
199+ self._keys[lkey] = key;
200+
201+ if (isBool(value))
202+ self._values[lkey] = getBool(value);
203+ else
204+ self._values[lkey] = value;
205+ }
206+
207+ self.remove = function (key) {
208+ const lkey = String(key).toLowerCase();
209+ delete self._keys[lkey];
210+ delete self._values[lkey];
211+ }
212+
213+ self.containsKey = function (key) {
214+ const lkey = String(key).toLowerCase();
215+
216+ for (var property in self._values) {
217+ if (self._values.hasOwnProperty(property) && property === lkey)
218+ return true;
219+ }
220+
221+ return false;
222+ }
223+
224+ self.containsValue = function (value) {
225+ for (var property in self._values) {
226+ if (self._values.hasOwnProperty(property) && self._values[property] === value)
227+ return true;
228+ }
229+
230+ return false;
231+ }
232+
233+ self.clear = function () {
234+ self._keys = [];
235+ self._values = [];
236+ }
237+
238+ self.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) {
239+ const keyValuePairs = [];
240+
241+ for (var property in self._values) {
242+ if (self._values.hasOwnProperty(property))
243+ keyValuePairs[self._keys[property]] = self._values[property];
244+ }
245+
246+ return keyValuePairs.joinKeyValuePairs(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter);
247+ }
248+
249+ self.pushAll = function (source) {
250+ for (var property in source)
251+ if (source.hasOwnProperty(property))
252+ self.set(property, source[property]);
253+ }
254+
255+ self.toObservableDictionary = function (useLowerKeys) {
256+ // See ko.observableDictionary.js
257+ const observableDictionary = new ko.observableDictionary();
258+
259+ for (var property in self._values) {
260+ if (self._values.hasOwnProperty(property))
261+ observableDictionary.push(useLowerKeys ? property : self._keys[property], self._values[property]);
262+ }
263+
264+ return observableDictionary;
265+ }
266+
267+ self.updateObservableDictionary = function (observableDictionary, useLowerKeys) {
268+ for (var property in self._values) {
269+ if (self._values.hasOwnProperty(property))
270+ observableDictionary.set(useLowerKeys ? property : self._keys[property], self._values[property]);
271+ }
272+ }
273+
274+ // Construction
275+ if (source instanceof Dictionary) {
276+ for (var property in source._values)
277+ if (source._values.hasOwnProperty(property))
278+ self.set(source._keys[property], source._values[property]);
279+ }
280+ else {
281+ for (var property in source) {
282+ if (source.hasOwnProperty(property))
283+ self.set(property, source[property]);
284+ }
285+ }
286+}
287+
288+Dictionary.fromObservableDictionary = function (observableDictionary) {
289+ var dictionary = new Dictionary();
290+ dictionary.pushAll(observableDictionary.toJSON());
291+ return dictionary;
292+}
293+
294+// String functions
295+function isEmpty(str) {
296+ return !str || String(str).length === 0;
297+}
298+
299+String.prototype.truncate = function (limit) {
300+ const text = this.trim();
301+
302+ if (text.length > limit)
303+ return text.substr(0, limit - 3) + "...";
304+
305+ return text;
306+}
307+
308+String.prototype.replaceAll = function (findText, replaceWith, ignoreCase) {
309+ return this.replace(
310+ new RegExp(findText.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, "\\$&"), (ignoreCase ? "gi" : "g")),
311+ (typeof replaceWith == "string") ? replaceWith.replace(/\$/g, "$$$$") : replaceWith);
312+}
313+
314+String.prototype.padLeft = function (totalWidth, paddingChar) {
315+ if (totalWidth > this.length)
316+ return Array(totalWidth - this.length + 1).join(paddingChar || " ") + this;
317+
318+ return this;
319+}
320+
321+String.prototype.padRight = function (totalWidth, paddingChar) {
322+ if (totalWidth > this.length)
323+ return this + Array(totalWidth - this.length + 1).join(paddingChar || " ");
324+
325+ return this;
326+}
327+
328+String.prototype.countOccurrences = function (searchString) {
329+ return (this.split(searchString).length - 1);
330+}
331+
332+if (!String.prototype.startsWith) {
333+ String.prototype.startsWith = function (searchString, position) {
334+ position = position || 0;
335+ return this.substr(position, searchString.length) === searchString;
336+ }
337+}
338+
339+String.prototype.regexEncode = function () {
340+ return "\\u" + this.charCodeAt(0).toString(16).padLeft(4, "0");
341+}
342+
343+// Returns a Dictionary of the parsed key/value pair expressions from a string. Parameter pairs are delimited by keyValueDelimiter
344+// and multiple pairs separated by parameterDelimiter. Supports encapsulated nested expressions.
345+String.prototype.parseKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter, ignoreDuplicateKeys) {
346+ if (!parameterDelimiter)
347+ parameterDelimiter = ";";
348+
349+ if (!keyValueDelimiter)
350+ keyValueDelimiter = "=";
351+
352+ if (!startValueDelimiter)
353+ startValueDelimiter = "{";
354+
355+ if (!endValueDelimiter)
356+ endValueDelimiter = "}";
357+
358+ if (ignoreDuplicateKeys === undefined)
359+ ignoreDuplicateKeys = true;
360+
361+ if (parameterDelimiter === keyValueDelimiter ||
362+ parameterDelimiter === startValueDelimiter ||
363+ parameterDelimiter === endValueDelimiter ||
364+ keyValueDelimiter === startValueDelimiter ||
365+ keyValueDelimiter === endValueDelimiter ||
366+ startValueDelimiter === endValueDelimiter)
367+ throw "All delimiters must be unique";
368+
369+ const escapedParameterDelimiter = parameterDelimiter.regexEncode();
370+ const escapedKeyValueDelimiter = keyValueDelimiter.regexEncode();
371+ const escapedStartValueDelimiter = startValueDelimiter.regexEncode();
372+ const escapedEndValueDelimiter = endValueDelimiter.regexEncode();
373+ const backslashDelimiter = "\\".regexEncode();
374+
375+ var keyValuePairs = new Dictionary();
376+ var escapedValue = [];
377+ var valueEscaped = false;
378+ var delimiterDepth = 0;
379+
380+ // Escape any parameter or key/value delimiters within tagged value sequences
381+ // For example, the following string:
382+ // "normalKVP=-1; nestedKVP={p1=true; p2=false}")
383+ // would be encoded as:
384+ // "normalKVP=-1; nestedKVP=p1\\u003dtrue\\u003b p2\\u003dfalse")
385+ for (var i = 0; i < this.length; i++) {
386+ var character = this[i];
387+
388+ if (character === startValueDelimiter) {
389+ if (!valueEscaped) {
390+ valueEscaped = true;
391+ continue; // Don't add tag start delimiter to final value
392+ }
393+
394+ // Handle nested delimiters
395+ delimiterDepth++;
396+ }
397+
398+ if (character === endValueDelimiter) {
399+ if (valueEscaped) {
400+ if (delimiterDepth > 0) {
401+ // Handle nested delimiters
402+ delimiterDepth--;
403+ }
404+ else {
405+ valueEscaped = false;
406+ continue; // Don't add tag stop delimiter to final value
407+ }
408+ }
409+ else {
410+ throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered end value delimiter \"" + endValueDelimiter + "\" before start value delimiter \"" + startValueDelimiter + "\".";
411+ }
412+ }
413+
414+ if (valueEscaped) {
415+ // Escape any delimiter characters inside nested key/value pair
416+ if (character === parameterDelimiter)
417+ escapedValue.push(escapedParameterDelimiter);
418+ else if (character === keyValueDelimiter)
419+ escapedValue.push(escapedKeyValueDelimiter);
420+ else if (character === startValueDelimiter)
421+ escapedValue.push(escapedStartValueDelimiter);
422+ else if (character === endValueDelimiter)
423+ escapedValue.push(escapedEndValueDelimiter);
424+ else if (character === "\\")
425+ escapedValue.push(backslashDelimiter);
426+ else
427+ escapedValue.push(character);
428+ }
429+ else {
430+ if (character === "\\")
431+ escapedValue.push(backslashDelimiter);
432+ else
433+ escapedValue.push(character);
434+ }
435+ }
436+
437+ if (delimiterDepth !== 0 || valueEscaped) {
438+ // If value is still escaped, tagged expression was not terminated
439+ if (valueEscaped)
440+ delimiterDepth = 1;
441+
442+ throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered more " +
443+ (delimiterDepth > 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + " than " +
444+ (delimiterDepth < 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + ".";
445+ }
446+
447+ // Parse key/value pairs from escaped value
448+ var pairs = escapedValue.join("").split(parameterDelimiter);
449+
450+ for (var i = 0; i < pairs.length; i++) {
451+ // Separate key from value
452+ var elements = pairs[i].split(keyValueDelimiter);
453+
454+ if (elements.length === 2) {
455+ // Get key
456+ var key = elements[0].trim();
457+
458+ // Get unescaped value
459+ var unescapedValue = elements[1].trim().
460+ replaceAll(escapedParameterDelimiter, parameterDelimiter).
461+ replaceAll(escapedKeyValueDelimiter, keyValueDelimiter).
462+ replaceAll(escapedStartValueDelimiter, startValueDelimiter).
463+ replaceAll(escapedEndValueDelimiter, endValueDelimiter).
464+ replaceAll(backslashDelimiter, "\\");
465+
466+ // Add key/value pair to dictionary
467+ if (ignoreDuplicateKeys) {
468+ // Add or replace key elements with unescaped value
469+ keyValuePairs.set(key, unescapedValue);
470+ }
471+ else {
472+ // Add key elements with unescaped value throwing an exception for encountered duplicate keys
473+ if (keyValuePairs.containsKey(key))
474+ throw "Failed to parse key/value pairs: duplicate key encountered. Key \"" + key + "\" is not unique within the string: \"" + this + "\"";
475+
476+ keyValuePairs.set(key, unescapedValue);
477+ }
478+ }
479+ }
480+
481+ return keyValuePairs;
482+}
483+
484+// Date Functions
485+Date.prototype.addDays = function (days) {
486+ return new Date(this.setDate(this.getDate() + days));
487+}
488+
489+Date.prototype.toUTC = function () {
490+ this.setMinutes(this.getMinutes() - this.getTimezoneOffset());
491+ return this;
492+}
493+
494+Date.prototype.daysBetween = function (startDate) {
495+ const millisecondsPerDay = 24 * 60 * 60 * 1000;
496+ return (this.toUTC() - startDate.toUTC()) / millisecondsPerDay;
497+}
498+
499+String.prototype.toDate = function () {
500+ return new Date(Date.parse(this));
501+};
502+
503+String.prototype.formatDate = function (format, utc) {
504+ return formatDate(this.toDate(), format, utc);
505+};
506+
507+Date.prototype.formatDate = function (format, utc) {
508+ return formatDate(this, format, utc);
509+};
510+
511+function formatDate(date, format, utc) {
512+ if (typeof date === "string")
513+ return formatDate(date.toDate(), format, utc);
514+
515+ if (format === undefined && DateTimeFormat != undefined)
516+ format = DateTimeFormat;
517+
518+ if (utc === undefined)
519+ utc = true;
520+
521+ var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
522+ var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
523+ var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
524+ var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
525+
526+ function ii(i, len) {
527+ var ss = i + "";
528+ len = len || 2;
529+ while (ss.length < len) ss = "0" + ss;
530+ return ss;
531+ }
532+
533+ var y = utc ? date.getUTCFullYear() : date.getFullYear();
534+ format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y);
535+ format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2));
536+ format = format.replace(/(^|[^\\])y/g, "$1" + y);
537+
538+ var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
539+ format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]);
540+ format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]);
541+ format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M));
542+ format = format.replace(/(^|[^\\])M/g, "$1" + M);
543+
544+ var d = utc ? date.getUTCDate() : date.getDate();
545+ format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]);
546+ format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]);
547+ format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d));
548+ format = format.replace(/(^|[^\\])d/g, "$1" + d);
549+
550+ var H = utc ? date.getUTCHours() : date.getHours();
551+ format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H));
552+ format = format.replace(/(^|[^\\])H/g, "$1" + H);
553+
554+ var h = H > 12 ? H - 12 : H === 0 ? 12 : H;
555+ format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h));
556+ format = format.replace(/(^|[^\\])h/g, "$1" + h);
557+
558+ var m = utc ? date.getUTCMinutes() : date.getMinutes();
559+ format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m));
560+ format = format.replace(/(^|[^\\])m/g, "$1" + m);
561+
562+ var s = utc ? date.getUTCSeconds() : date.getSeconds();
563+ format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s));
564+ format = format.replace(/(^|[^\\])s/g, "$1" + s);
565+
566+ var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds();
567+ format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3));
568+ f = Math.round(f / 10);
569+ format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f));
570+ f = Math.round(f / 10);
571+ format = format.replace(/(^|[^\\])f/g, "$1" + f);
572+
573+ var T = H < 12 ? "AM" : "PM";
574+ format = format.replace(/(^|[^\\])TT+/g, "$1" + T);
575+ format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0));
576+
577+ var t = T.toLowerCase();
578+ format = format.replace(/(^|[^\\])tt+/g, "$1" + t);
579+ format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0));
580+
581+ var tz = -date.getTimezoneOffset();
582+ var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-";
583+ if (!utc) {
584+ tz = Math.abs(tz);
585+ var tzHrs = Math.floor(tz / 60);
586+ var tzMin = tz % 60;
587+ K += ii(tzHrs) + ":" + ii(tzMin);
588+ }
589+
590+ format = format.replace(/(^|[^\\])K/g, "$1" + K);
591+ var day = (utc ? date.getUTCDay() : date.getDay()) + 1;
592+
593+ format = format.replace(new RegExp(dddd[0], "g"), dddd[day]);
594+ format = format.replace(new RegExp(ddd[0], "g"), ddd[day]);
595+
596+ format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]);
597+ format = format.replace(new RegExp(MMM[0], "g"), MMM[M]);
598+
599+ format = format.replace(/\\(.)/g, "$1");
600+
601+ return format;
602+};
603+
604+// jQuery extensions
605+$.fn.paddingHeight = function () {
606+ return this.outerHeight(true) - this.height();
607+}
608+
609+$.fn.paddingWidth = function () {
610+ return this.outerWidth(true) - this.width();
611+}
612+
613+// Cell truncations should only be used with .table-cell-hard-wrap style
614+$.fn.truncateToWidth = function (text, rows) {
615+ if (isEmpty(text))
616+ return "";
617+
618+ if (!rows)
619+ rows = 1;
620+
621+ textMetrics.font = this.css("font");
622+
623+ var targetWidth = this.innerWidth();
624+ var textWidth = textMetrics.measureText(text).width;
625+
626+ if (rows > 1)
627+ targetWidth *= ((isIE ? 0.45 : 0.75) * rows);
628+
629+ var limit = Math.min(text.length, Math.ceil(targetWidth / (textWidth / text.length)));
630+
631+ while (textWidth > targetWidth && limit > 1) {
632+ limit--;
633+ text = text.truncate(limit);
634+ textWidth = textMetrics.measureText(text).width;
635+ }
636+
637+ return text;
638+}
639+
640+$.fn.visible = function () {
641+ return this.css("visibility", "visible");
642+}
643+
644+$.fn.invisible = function () {
645+ return this.css("visibility", "hidden");
646+}
647+
648+// The following target arrays of promises
649+$.fn.whenAny = function () {
650+ var finish = $.Deferred();
651+
652+ if (this.length === 0)
653+ finish.resolve();
654+ else
655+ $.each(this, function (index, deferred) {
656+ deferred.done(finish.resolve);
657+ });
658+
659+ return finish.promise();
660+}
661+
662+$.fn.whenAll = function () {
663+ if (this.length > 0)
664+ return $.when.apply($, this);
665+
666+ return $.Deferred().resolve().promise();
667+}
Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js+568−0
@@ -0,0 +1,568 @@
1+//******************************************************************************************************
2+// pagedViewModel.js - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/23/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+// Paged view model base class scripts
25+"use strict";
26+
27+var RecordMode = {
28+ View: 0,
29+ Edit: 1,
30+ AddNew: 2
31+}
32+
33+// Define paged view model class
34+function PagedViewModel() {
35+ const self = this;
36+
37+ // Configuration fields
38+ self.labelField = "{name}"; // Field name that best represents row for delete confirmation
39+ self.primaryKeyFields = ["{id}"]; // Primary key field names array
40+ self.defaultSortField = "{id}"; // Default sort field
41+ self.defaultSortAscending = true; // Default sort ascending flag
42+ self.initialFocusField = ""; // Initial add/edit field with focus
43+ self.modelName = "{name}"; // Name of model used for cookie names, defaults to page title
44+
45+ // Observable fields
46+ self.pageRecords = ko.observableArray(); // Records queried for current page
47+ self.recordCount = ko.observable(0); // Queried record count
48+ self.sortField = ko.observable(self.defaultSortField); // Current sort field, persisted per model
49+ self.sortAscending = ko.observable(self.defaultSortAscending); // Current sort ascending flag, persisted per model
50+ self.unassignedFieldCount = ko.observable(0); // Number of bound fields with missing data
51+ self.dataHubIsConnected = ko.observable(false); // Data hub connected observable flag - externally managed
52+ self.errors = ko.validation.group([ko.observable(0)]); // Validation errors array
53+ self.canEdit = ko.observable(true); // Can edit flag - normally controlled by external roles
54+ self.canAddNew = ko.observable(true); // Can add new flag - normally controlled by external roles
55+ self.canDelete = ko.observable(true); // Can delete flag - normally controlled by external roles
56+
57+ // Internal fields
58+ self._currentPageSize = ko.observable(1);
59+ self._currentPage = ko.observable(0);
60+ self._currentRecord = ko.observable();
61+ self._recordMode = ko.observable(RecordMode.View);
62+ self._isDirty = ko.observable(false);
63+ self._columnWidths = [];
64+
65+ // Properties
66+
67+ // Gets or sets the number records to display on the page
68+ self.currentPageSize = ko.pureComputed({
69+ read: self._currentPageSize,
70+ write: function (value) {
71+ if (value < 1)
72+ value = 1;
73+
74+ if (value !== self._currentPageSize()) {
75+ self._currentPageSize(value);
76+
77+ // Validate current page after page size change
78+ if (self.currentPage())
79+ self.currentPage(self.currentPage());
80+ }
81+ },
82+ owner: self
83+ });
84+
85+ // Gets or sets the current page number
86+ self.currentPage = ko.pureComputed({
87+ read: self._currentPage,
88+ write: function (value) {
89+ if (value < 1)
90+ value = 1;
91+ else if (value > self.totalPages())
92+ value = self.totalPages();
93+
94+ // Requery records for page when current page changes
95+ if (value !== self._currentPage()) {
96+ self._currentPage(value);
97+ self.queryPageRecords();
98+ }
99+ },
100+ owner: self
101+ });
102+
103+ // Gets or sets current record on page
104+ self.currentRecord = ko.pureComputed({
105+ read: self._currentRecord,
106+ write: function(value) {
107+ self._currentRecord(value);
108+ self.isDirty(false);
109+ $(self).trigger("currentRecordChanged");
110+ self.applyValidationParameters();
111+
112+ // Watch for changes to fields in current record
113+ ko.watch(self._currentRecord(), function (parents, child, item) {
114+ self.isDirty(true);
115+ $(self).trigger("currentRecordUpdated", [child]);
116+
117+ // Recalculate unassigned field count after bindings may have been updated by field change
118+ setTimeout(self.calculateUnassignedFieldCount, 10);
119+ });
120+ },
121+ owner: self
122+ });
123+
124+ // Gets or sets the record mode, i.e., view, edit or add new
125+ self.recordMode = ko.pureComputed({
126+ read: self._recordMode,
127+ write: function (newMode) {
128+ const oldMode = self._recordMode();
129+
130+ if (newMode !== oldMode) {
131+ self._recordMode(newMode);
132+
133+ if (oldMode === RecordMode.View && newMode === RecordMode.Edit)
134+ self.setFocusOnInitialField();
135+
136+ $(self).trigger("recordModeChanged", [oldMode, newMode]);
137+ }
138+ },
139+ owner: self
140+ });
141+
142+ // Gets or sets current record changed flag
143+ self.isDirty = ko.pureComputed({
144+ read: self._isDirty,
145+ write: function (value) {
146+ if (value === undefined)
147+ value = true;
148+
149+ self._isDirty(value);
150+ self.calculateUnassignedFieldCount();
151+ },
152+ owner: self
153+ });
154+
155+ // Gets total number pages based on total record count and current page size
156+ self.totalPages = ko.pureComputed(function () {
157+ return Math.max(Math.ceil(self.recordCount() / self.currentPageSize()), 1);
158+ });
159+
160+ // Gets flag that determines if current page is first page
161+ self.onFirstPage = ko.pureComputed(function () {
162+ return self.currentPage() <= 1;
163+ });
164+
165+ // Gets flag that determines if current page is last page
166+ self.onLastPage = ko.pureComputed(function () {
167+ return self.currentPage() >= self.totalPages();
168+ });
169+
170+ // Return validation errors, i.e., non-required field errors
171+ self.validationErrors = ko.pureComputed(function() {
172+ return self.errors().length - self.unassignedFieldCount();
173+ }).extend({ notify: "always" });
174+
175+ // Delegates
176+ self.queryRecordCount = function () { };
177+ self.queryRecords = function (/* sortField, ascending, page, pageSize */) { };
178+ self.deleteRecord = function (/* keyValues[] */) { };
179+ self.newRecord = function () { };
180+ self.addNewRecord = function (/* record */) { };
181+ self.updateRecord = function (/* record */) { };
182+ self.applyValidationParameters = function () { };
183+
184+ // Setters needed to assign delegate properties, 'cause Javascript
185+ self.setQueryRecordCount = function (queryRecordCountFunction) {
186+ self.queryRecordCount = queryRecordCountFunction;
187+ }
188+
189+ self.setQueryRecords = function (queryRecordsFunction) {
190+ self.queryRecords = queryRecordsFunction;
191+ }
192+
193+ self.setDeleteRecord = function (deleteRecordFunction) {
194+ self.deleteRecord = deleteRecordFunction;
195+ }
196+
197+ self.setNewRecord = function (newRecordFunction) {
198+ self.newRecord = newRecordFunction;
199+ }
200+
201+ self.setAddNewRecord = function (addNewRecordFunction) {
202+ self.addNewRecord = addNewRecordFunction;
203+ }
204+
205+ self.setUpdateRecord = function (updateRecordFunction) {
206+ self.updateRecord = updateRecordFunction;
207+ }
208+
209+ self.setApplyValidationParameters = function (applyValidationParametersFunction) {
210+ self.applyValidationParameters = applyValidationParametersFunction;
211+ }
212+
213+ // Methods
214+ self.initialize = function () {
215+ // Restore any previous sort order
216+ const lastSortField = Cookies.get(self.modelName + "!LastSortField");
217+ const lastSortAscending = Cookies.get(self.modelName + "!LastSortAscending");
218+
219+ if (lastSortField === undefined)
220+ self.sortField(self.defaultSortField);
221+ else
222+ self.sortField(lastSortField);
223+
224+ if (lastSortAscending === undefined)
225+ self.sortAscending(self.defaultSortAscending);
226+ else
227+ self.sortAscending(lastSortAscending === "true");
228+
229+ if (self.dataHubIsConnected()) {
230+ // Query total record count
231+ self.queryRecordCount().done(function (count) {
232+ // Update record count observable
233+ self.recordCount(count);
234+
235+ // Force page refresh when record count has been updated
236+ const currentPage = self.currentPage();
237+ self._currentPage(0);
238+ self.currentPage(currentPage);
239+ });
240+
241+ // Initialize current record with an empty row
242+ self.newRecord().done(function (emptyRecord) {
243+ self.deriveObservableRecord(emptyRecord).done(function (observableRecord) {
244+ self.currentRecord(observableRecord);
245+ });
246+ });
247+ }
248+
249+ // Initialize column widths array
250+ self._columnWidths = [];
251+ const columns = $("#recordRow").find("td");
252+
253+ for (let i = 0; i < columns.length; i++) {
254+ self._columnWidths.push($(columns[i]).width());
255+ }
256+ }
257+
258+ self.calculatePageSize = function (forceRefresh) {
259+ // Calculate total number of table rows that will fit within current page height
260+ const remainingHeight = calculateRemainingBodyHeight() -
261+ $("#contentWell").paddingHeight() -
262+ $("#responsiveTableDiv").paddingHeight() -
263+ $("#recordsTable").paddingHeight() -
264+ $("#pageControlsRow").outerHeight(true);
265+
266+ // Estimate page size based on height of first record row
267+ const firstRow = $("#recordRow");
268+ var pageSize = (remainingHeight / firstRow.outerHeight(true)).truncate();
269+
270+ if (!pageSize || isNaN(pageSize) || !isFinite(pageSize) || pageSize < 1)
271+ pageSize = 1;
272+
273+ if (forceRefresh === undefined)
274+ forceRefresh = false;
275+
276+ // Check for dynamic Bootstrap column resizing, in which case we need to refresh data
277+ // for cases where binding may be truncating data lengths, see $.fn.truncateToWidth
278+ const columns = firstRow.find("td");
279+ var columnWidth;
280+
281+ for (let i = 0; i < columns.length; i++) {
282+ columnWidth = $(columns[i]).width();
283+
284+ if (self._columnWidths[i] !== columnWidth) {
285+ self._columnWidths[i] = columnWidth;
286+ forceRefresh = true;
287+ }
288+ }
289+
290+ if (pageSize !== self.currentPageSize() || forceRefresh) {
291+ const currentPage = self.currentPage();
292+
293+ // Updating page size will validate current page number
294+ self.currentPageSize(pageSize);
295+
296+ // Requery data for page unless current page was dynamically changed
297+ // by page size update and has reloaded data already
298+ if (currentPage === self.currentPage())
299+ self.queryPageRecords();
300+ }
301+ }
302+
303+ self.calculateUnassignedFieldCount = function () {
304+ // Derive unassigned field count based on existence of Bootstrap "has-error" class
305+ self.unassignedFieldCount($("#addNewEditDialog div.form-group.has-error").length);
306+ }
307+
308+ self.refreshValidationErrors = function () {
309+ // Force re-evaluation of validation errors property
310+ self.unassignedFieldCount.valueHasMutated();
311+
312+ // Make sure any initial validation error messages are visible
313+ self.errors.showAllMessages();
314+ }
315+
316+ // Convert observable object to a simple Javascript record
317+ self.deriveJSRecord = function () {
318+ return $.Deferred(function (deferred) {
319+ const observableRecord = self.currentRecord();
320+ const promises = [];
321+
322+ // Allow customization of observable record before conversion
323+ $(self).trigger("derivingJSRecord", [observableRecord, promises]);
324+
325+ $(promises).whenAll().done(function () {
326+ deferred.resolve(ko.mapping.toJS(observableRecord));
327+ });
328+ }).promise();
329+ }
330+
331+ // Convert simple Javascript record to an observable object
332+ self.deriveObservableRecord = function (record) {
333+ return $.Deferred(function (deferred) {
334+ const observableRecord = ko.mapping.fromJS(record);
335+ const promises = [];
336+
337+ // Allow customization of new observable record
338+ $(self).trigger("derivingObservableRecord", [observableRecord, promises]);
339+
340+ $(promises).whenAll().done(function () {
341+ // Apply validation binding to current observable record
342+ self.errors = ko.validation.group(observableRecord);
343+ self.refreshValidationErrors();
344+ deferred.resolve(observableRecord);
345+ });
346+ }).promise();
347+ }
348+
349+ self.setFocusOnInitialField = function () {
350+ if (!isEmpty(self.initialFocusField))
351+ $("#" + self.initialFocusField).focus();
352+ }
353+
354+ self.nextPage = function () {
355+ if (self.currentPage() < self.totalPages())
356+ self.currentPage(self.currentPage() + 1);
357+ }
358+
359+ self.previousPage = function () {
360+ if (self.currentPage() > 1)
361+ self.currentPage(self.currentPage() - 1);
362+ }
363+
364+ self.updateSortOrder = function (fieldName, ascending) {
365+ self.sortField(fieldName);
366+ self.sortAscending(ascending);
367+ self.queryPageRecords();
368+
369+ // Save last sort order
370+ Cookies.set(self.modelName + "!LastSortField", self.sortField(), { expires: 365 });
371+ Cookies.set(self.modelName + "!LastSortAscending", self.sortAscending().toString(), { expires: 365 });
372+ }
373+
374+ self.isSortOrder = function (fieldName, ascending) {
375+ return self.sortField().toUpperCase() === fieldName.toUpperCase() && self.sortAscending() === ascending;
376+ }
377+
378+ self.queryPageRecords = function () {
379+ if (self.dataHubIsConnected())
380+ self.queryRecords(self.sortField(), self.sortAscending(), self.currentPage(), self.currentPageSize()).done(function (records) {
381+ $(self).trigger("pageRecordsQueried", [records]);
382+ self.pageRecords.removeAll();
383+ self.pageRecords(records);
384+ refreshHubDependentControlState();
385+ $("[id='recordRow']").css("visibility", "visible");
386+ $("#loadingDataLabel").hide();
387+
388+ // Validate proper page size after any record refresh
389+ setTimeout(self.calculatePageSize, 150);
390+ }).fail(function (error) {
391+ showErrorMessage(error);
392+ });
393+ }
394+
395+ self.removePageRecord = function (record) {
396+ if (!self.canDelete())
397+ return;
398+
399+ if (self.dataHubIsConnected() && confirm("Are you sure you want to delete \"" + record[self.labelField] + "\"?")) {
400+ const keyValues = [];
401+
402+ for (var i = 0; i < self.primaryKeyFields.length; i++) {
403+ keyValues.push(record[self.primaryKeyFields[i]]);
404+ }
405+
406+ self.deleteRecord(keyValues).done(function () {
407+ self.pageRecords.remove(record);
408+ self.initialize();
409+ $(self).trigger("recordDeleted", [record]);
410+ showInfoMessage("Deleted record...");
411+ }).fail(function (error) {
412+ showErrorMessage(error);
413+ });
414+ }
415+ }
416+
417+ self.savePageRecord = function () {
418+ switch (self.recordMode()) {
419+ case RecordMode.Edit:
420+ self.saveEditedRecord();
421+ break;
422+ case RecordMode.AddNew:
423+ self.saveNewRecord();
424+ break;
425+ }
426+ }
427+
428+ self.saveEditedRecord = function () {
429+ if (!self.canEdit())
430+ return;
431+
432+ if (self.dataHubIsConnected()) {
433+ self.deriveJSRecord().done(function (record) {
434+ self.updateRecord(record).done(function () {
435+ self.initialize();
436+ $(self).trigger("recordSaved", [record, false]);
437+ showInfoMessage("Saved updated record...");
438+ }).fail(function (error) {
439+ showErrorMessage(error);
440+ });
441+ });
442+ }
443+ }
444+
445+ self.saveNewRecord = function () {
446+ if (!self.canAddNew())
447+ return;
448+
449+ if (self.dataHubIsConnected()) {
450+ self.deriveJSRecord().done(function (record) {
451+ self.addNewRecord(record).done(function () {
452+ self.initialize();
453+ $(self).trigger("recordSaved", [record, true]);
454+ showInfoMessage("Saved new record...");
455+ }).fail(function (error) {
456+ showErrorMessage(error);
457+ });
458+ });
459+ }
460+ }
461+
462+ self.viewPageRecord = function (record) {
463+ self.deriveObservableRecord(record).done(function (observableRecord) {
464+ self.recordMode(RecordMode.View);
465+ self.currentRecord(observableRecord);
466+ $("#addNewEditDialog").modal("show");
467+ });
468+ }
469+
470+ self.editPageRecord = function (record) {
471+ if (!self.canEdit())
472+ return;
473+
474+ self.deriveObservableRecord(record).done(function (observableRecord) {
475+ self.recordMode(RecordMode.Edit);
476+ self.currentRecord(observableRecord);
477+ $("#addNewEditDialog").modal("show");
478+ });
479+ }
480+
481+ self.addPageRecord = function () {
482+ if (!self.canAddNew())
483+ return;
484+
485+ if (self.dataHubIsConnected()) {
486+ self.newRecord().done(function (emptyRecord) {
487+ // Raise event to allow any new record initialization
488+ $(self).trigger("newRecord", [emptyRecord]);
489+
490+ self.deriveObservableRecord(emptyRecord).done(function (observableRecord) {
491+ self.recordMode(RecordMode.AddNew);
492+ self.currentRecord(observableRecord);
493+ $("#addNewEditDialog").modal("show");
494+ });
495+ });
496+ }
497+ }
498+
499+ self.cancelPageRecord = function () {
500+ if (!self.isDirty() || confirm("Are you sure you want to discard unsaved changes?"))
501+ $("#addNewEditDialog").modal("hide");
502+ }
503+};
504+
505+// Define page scoped view model instance
506+var viewModel = new PagedViewModel();
507+
508+(function ($, viewPort) {
509+ $("#bodyContainer").addClass("fill-height");
510+
511+ $("#titleText").html("Records: <span data-bind='text: recordCount'>calculating...</span>");
512+
513+ $("#firstPageButton").click(function () {
514+ viewModel.currentPage(1);
515+ });
516+
517+ $("#previousPageButton").click(function () {
518+ viewModel.previousPage();
519+ });
520+
521+ $("#nextPageButton").click(function () {
522+ viewModel.nextPage();
523+ });
524+
525+ $("#lastPageButton").click(function () {
526+ viewModel.currentPage(viewModel.totalPages());
527+ });
528+
529+ $(window).on("hubConnected", function (event) {
530+ viewModel.dataHubIsConnected(true);
531+ viewModel.initialize();
532+ });
533+
534+ $(window).on("hubDisconnected", function (event) {
535+ viewModel.dataHubIsConnected(false);
536+ });
537+
538+ $(window).on("messageVisibiltyChanged", function (event) {
539+ viewModel.calculatePageSize();
540+ });
541+
542+ $("#addNewEditDialog").on("shown.bs.modal", function () {
543+ viewModel.setFocusOnInitialField();
544+ $("[data-toggle='tooltip']").tooltip();
545+ });
546+
547+ $(window).resize(
548+ viewPort.changed(function () {
549+ viewModel.calculatePageSize();
550+ }));
551+
552+ viewModel.calculatePageSize();
553+
554+ ko.validation.rules.pattern.message = "Invalid";
555+
556+ ko.validation.init({
557+ registerExtenders: true,
558+ messagesOnModified: true,
559+ insertMessages: true,
560+ parseInputAttributes: true,
561+ allowHtmlMessages: true,
562+ messageTemplate: null,
563+ grouping: { deep: true, observable: true, live: true }
564+ }, true);
565+
566+ ko.applyBindings(viewModel);
567+
568+})(jQuery, ResponsiveBootstrapToolkit);
Source/Libraries/GSF.Web/Model/Views/PagedViewModel.cshtml+348−0
@@ -0,0 +1,348 @@
1+@*******************************************************************************************************
2+// PagedViewModel.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/22/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using System
24+@using System.Collections.Generic
25+@using System.Security.Policy
26+@using System.Text
27+@using GSF
28+@using GSF.Web
29+@using GSF.Web.Model
30+@using GSF.Web.Templating
31+@{
32+ if (ViewBag.PageControlScripts == null)
33+ {
34+ ViewBag.PageControlScripts = new StringBuilder();
35+ }
36+
37+ StringBuilder pageControlScripts = ViewBag.PageControlScripts;
38+
39+ // Setup page control scripts
40+ pageControlScripts.Append(Scripts.Render("~/js.bundle/knockout"));
41+ pageControlScripts.Append($"<script src=\"{Url.Content("~/Scripts/pagedViewModel.js")}\"></script>");
42+
43+ pageControlScripts.Append(@"
44+ <script id=""validationMessageTemplate"" type=""text/html""><!--
45+ --><em class=""validationPrompt"" data-bind=""validationMessage: field""></em>
46+ </script>
47+ <script>
48+ $(function () {
49+ $(""#clearSortOrder"").click(function () {
50+ viewModel.updateSortOrder(viewModel.defaultSortField, viewModel.defaultSortAscending);
51+ });
52+ ");
53+
54+ // Check for optional page behaviors
55+
56+ // Hide the add new button
57+ bool hideAddNewButton = ViewBag.HideAddNewButton ?? false;
58+
59+ // Hide unauthorized controls as disallowed by Edit/AddNew/Delete roles - otherwise unauthorized controls will be disabled
60+ bool hideUnauthorizedControls = ViewBag.HideUnauthorizedControls ?? false;
61+
62+ // Auto pop-up add new dialog
63+ bool autoShowAddNew = ViewBag.RouteID?.Equals("AddNew", StringComparison.OrdinalIgnoreCase) ?? false;
64+
65+ bool showIsDeletedToggleLink = false;
66+ bool showingDeletedRecords = false;
67+
68+ // Check for existence of ShowDeleted property to control toggle link visibility
69+ if (ViewBag.ShowDeleted != null)
70+ {
71+ showIsDeletedToggleLink = true;
72+ showingDeletedRecords = Convert.ToBoolean(ViewBag.ShowDeleted);
73+ }
74+
75+ // Get current allowed create / update / delete states for user - default to none.
76+ // Note that overriding view bag settings will only affect UI, back end security
77+ // will still be applicable
78+ bool canEdit = ViewBag.CanEdit ?? false;
79+ bool canAddNew = ViewBag.CanAddNew ?? false;
80+ bool canDelete = ViewBag.CanDelete ?? false;
81+}
82+@functions
83+{
84+ string SingularForm(string value)
85+ {
86+ if (value.EndsWith("ies", StringComparison.Ordinal))
87+ return value.Substring(0, value.Length - 3) + "y";
88+
89+ if (value.EndsWith("IES", StringComparison.Ordinal))
90+ return value.Substring(0, value.Length - 3) + "Y";
91+
92+ if (value.EndsWith("es", StringComparison.OrdinalIgnoreCase))
93+ return value.Substring(0, value.Length - 2);
94+
95+ if (value.EndsWith("s", StringComparison.OrdinalIgnoreCase))
96+ return value.Substring(0, value.Length - 1);
97+
98+ return value;
99+ }
100+
101+ string RemoveSpaces(string value)
102+ {
103+ return value.RemoveWhiteSpace().RemoveInvalidFileNameCharacters();
104+ }
105+
106+ string RenderSortScript(string fieldName, bool ascending)
107+ {
108+ const string ScriptFormat = @"
109+ $(""#sort{0} #{1}"").click(function () {{
110+ viewModel.updateSortOrder(""{0}"", {2});
111+ }});
112+ ";
113+
114+ return string.Format(ScriptFormat, fieldName, ascending ? "asc" : "desc", ascending.ToString().ToLower());
115+ }
116+}
117+@helper WriteFieldHeader(StringBuilder pageControlScripts, string fieldName, string labelName = null, string classes = null)
118+{
119+ if (string.IsNullOrEmpty(fieldName))
120+ {
121+ <th class="@Html.Raw(classes ?? "text-center")" nowrap>@Html.Raw(labelName ?? "")</th>
122+ }
123+ else
124+ {
125+ <th class="@Html.Raw(classes ?? "text-center")" id="sort@(fieldName)" nowrap>@Html.Raw(labelName ?? fieldName)&nbsp;
126+ <div class="btn-group-vertical btn-group-sort">
127+ <button type="button" class="btn" id="asc" title="Sort&nbsp;ascending..." data-bind="css: {'btn-primary': isSortOrder('@fieldName', true)}, enable: dataHubIsConnected"><span class="glyphicon glyphicon-chevron-up"></span></button>
128+ <button type="button" class="btn" id="desc" title="Sort&nbsp;descending..." data-bind="css: {'btn-primary': isSortOrder('@fieldName', false)}, enable: dataHubIsConnected"><span class="glyphicon glyphicon-chevron-down"></span></button>
129+ </div>
130+ </th>
131+
132+ pageControlScripts.Append(RenderSortScript(fieldName, true));
133+ pageControlScripts.Append(RenderSortScript(fieldName, false));
134+ }
135+}
136+<div class="well well-dynamic-content" id="contentWell" content-fill-height>
137+ <div class="table-responsive" id="responsiveTableDiv">
138+ <table class="table table-condensed table-hover" id="recordsTable">
139+ <thead>
140+ <tr>
141+ @foreach (string[] headerColumn in ViewBag.HeaderColumns) {
142+ @WriteFieldHeader(pageControlScripts, headerColumn[0], headerColumn[1], headerColumn[2])
143+ }
144+ <th class="text-center">
145+ <button type="button" class="btn btn-link btn-xs" style="line-height: 1.1" id="clearSortOrder" data-bind="enable: dataHubIsConnected">Clear<br/>Sort</button>
146+ </th>
147+ </tr>
148+ </thead>
149+ <tbody data-bind="foreach: pageRecords">
150+ <tr style="visibility: hidden" id="recordRow"@Html.Raw(ViewBag.ShowDeleted ?? false ? $" data-bind=\"css: {{'danger': {ViewBag.IsDeletedField}}}\"" : "")>
151+ @Html.Raw(ViewBag.BodyRows)
152+ </tr>
153+ </tbody>
154+ </table>
155+ <div id="loadingDataLabel">
156+ Loading&nbsp;&nbsp;<span class="glyphicon glyphicon-refresh glyphicon-spin"></span>
157+ </div>
158+ </div>
159+ <div class="pull-right" id="pageControlsRow">
160+ @if (!hideAddNewButton && (canAddNew || !hideUnauthorizedControls)) {
161+ <button type="button" class="btn btn-sm btn-primary pull-right" id="addRecordButton" @Html.Raw(canAddNew ? "data-bind=\"enable: dataHubIsConnected\"" : "disabled")>
162+ <span class="glyphicon glyphicon-plus"></span>&nbsp;&nbsp;Add&nbsp;New
163+ </button>
164+ <br />
165+ <hr class="half-break" />
166+ }
167+ <div class="btn-group btn-group-sm">
168+ <button type="button" class="btn btn-default" id="firstPageButton" data-bind="css: {'disabled': onFirstPage()}, enable: dataHubIsConnected() && !onFirstPage()"><span class="glyphicon glyphicon-backward"></span></button>
169+ <button type="button" class="btn btn-default" id="previousPageButton" data-bind="css: {'disabled': onFirstPage()}, enable: dataHubIsConnected() && !onFirstPage()"><span class="glyphicon glyphicon-triangle-left"></span></button>
170+ </div>
171+ <input type="number" class="content input-sm" style="padding: 0 0 0 5px; width: 55px" id="selectedPage" data-bind="numeric, textInput: currentPage, enable: dataHubIsConnected" value="1">
172+ &nbsp;of&nbsp;
173+ <span data-bind="text: totalPages">1</span>
174+ &nbsp;
175+ <div class="btn-group btn-group-sm">
176+ <button type="button" class="btn btn-default" id="nextPageButton" data-bind="css: {'disabled': onLastPage()}, enable: dataHubIsConnected() && !onLastPage()"><span class="glyphicon glyphicon-triangle-right"></span></button>
177+ <button type="button" class="btn btn-default" id="lastPageButton" data-bind="css: {'disabled': onLastPage()}, enable: dataHubIsConnected() && !onLastPage()"><span class="glyphicon glyphicon-forward"></span></button>
178+ </div>
179+ </div>
180+</div>
181+<div id="addNewEditDialog" class="modal fade" role="dialog">
182+ <div class="modal-dialog modal-lg">
183+ <div class="modal-content">
184+ <div class="modal-header" data-bind="css: {'modal-readonly': recordMode()===RecordMode.View}">
185+ <button type="button" class="close" id="dismissDialogButton">&times;</button>
186+ <h4 class="modal-title">
187+ <span data-bind="visible: recordMode()===RecordMode.View">View</span>
188+ <span data-bind="visible: recordMode()===RecordMode.Edit">Edit</span>
189+ <span data-bind="visible: recordMode()===RecordMode.AddNew">Add New</span>
190+ @SingularForm(ViewBag.Title ?? "")
191+ </h4>
192+ </div>
193+ <div class="modal-body auto-height" data-bind="with: currentRecord, validationOptions: {messageTemplate: 'validationMessageTemplate'}, css: {'modal-readonly': recordMode()===RecordMode.View}">
194+ <form role="form">
195+ @Html.Raw(ViewBag.AddNewEditDialog)
196+ </form>
197+ </div>
198+ <div class="modal-footer" data-bind="css: {'modal-readonly': recordMode()===RecordMode.View}">
199+ <em data-bind="visible: unassignedFieldCount() > 0 || validationErrors() > 0">
200+ <span data-bind="visible: unassignedFieldCount() > 0">Missing <span data-bind="text: unassignedFieldCount"></span> required field<span data-bind="visible: unassignedFieldCount() > 1">s</span></span>
201+ <span data-bind="visible: unassignedFieldCount() > 0 && validationErrors() > 0"> and </span>
202+ <span data-bind="visible: validationErrors() > 0"><span data-bind="text: validationErrors()"></span> validation error<span data-bind="visible: validationErrors() > 1">s</span></span>
203+ ...
204+ </em>
205+ @if (canEdit || !hideUnauthorizedControls) {
206+ <button type="button" class="btn btn-info" data-bind="visible: recordMode()===RecordMode.View@(Html.Raw(canEdit ? ", enable: dataHubIsConnected" : ""))" id="editRecordButton"@Html.Raw(canEdit ? "" : " disabled")>Edit</button>
207+ }
208+ @if (canEdit || canAddNew || !hideUnauthorizedControls) {
209+ <button type="submit" class="btn btn-primary" data-dismiss="modal" data-bind="visible: recordMode()!==RecordMode.View@(Html.Raw(canEdit || canAddNew ? ", disable: unassignedFieldCount() > 0 || validationErrors() > 0 || !dataHubIsConnected()" : ""))" id="saveRecordButton"@Html.Raw(canEdit || canAddNew ? "" : " disabled")>Save</button>
210+ }
211+ <button type="button" class="btn btn-default" id="cancelRecordButton">Cancel</button>
212+ </div>
213+ </div>
214+ </div>
215+</div>
216+@{
217+ DataContext<CSharpEmbeddedResource> dataContext = ViewBag.DataContext;
218+
219+ if (autoShowAddNew) {
220+ pageControlScripts.Append(@"
221+ // Add new pop-up requested via URL parameter
222+ setTimeout(viewModel.addPageRecord, 500);
223+ ");
224+ }
225+
226+ if (showIsDeletedToggleLink) {
227+ string baseURL = Request?.Url?.AbsolutePath ?? "/";
228+ baseURL = baseURL.Substring(0, baseURL.Length - (ViewBag.RouteID ?? "").Length);
229+
230+ if (showingDeletedRecords) {
231+ pageControlScripts.Append($@"
232+ $(""#titleText"").append(""<br /><div style='margin-top: 3px' class='small text-center'><a href='{baseURL}'>Hide Deleted</a></div>"");
233+ ");
234+ }
235+ else {
236+ pageControlScripts.Append($@"
237+ $(""#titleText"").append(""<br /><div style='margin-top: 3px' class='small text-center'><a href='{baseURL.EnsureEnd('/')}ShowDeleted'>Show Deleted</a></div>"");
238+ ");
239+ }
240+ }
241+
242+ pageControlScripts.Append(@"
243+ $(""#addRecordButton"").click(function () {
244+ viewModel.addPageRecord();
245+ });
246+
247+ $(""#editRecordButton"").click(function () {
248+ viewModel.recordMode(RecordMode.Edit);
249+ });
250+
251+ $(""#saveRecordButton"").click(function () {
252+ viewModel.savePageRecord();
253+ });
254+
255+ $(""#cancelRecordButton"").click(function () {
256+ viewModel.cancelPageRecord();
257+ });
258+
259+ $(""#dismissDialogButton"").click(function () {
260+ viewModel.cancelPageRecord();
261+ });
262+
263+ $(""#addNewEditDialog"").modal({show: false, backdrop: ""static"", keyboard: false});
264+
265+ $(""#addNewEditDialog"").on(""shown.bs.modal"", function () {
266+ $(""#addNewEditDialog .input-group.date"").datepicker({
267+ todayBtn: ""linked"",
268+ enableOnReadonly: false,
269+ autoclose: true
270+ });
271+ });
272+
273+ $(window).on(""beforeunload"", function () {
274+ if ($(""#addNewEditDialog"").hasClass(""in"") && viewModel.isDirty())
275+ return ""There are unsaved changes to this record."";
276+
277+ return undefined;
278+ });
279+ });
280+ ");
281+
282+ pageControlScripts.AppendFormat(@"
283+ viewModel.canEdit({0});
284+ viewModel.canAddNew({1});
285+ viewModel.canDelete({2});
286+
287+ viewModel.initialFocusField = ""{3}"";
288+ viewModel.modelName = ""{4}"";",
289+ canEdit.ToString().ToLower(),
290+ canAddNew.ToString().ToLower(),
291+ canDelete.ToString().ToLower(),
292+ dataContext.InitialFocusField,
293+ RemoveSpaces(ViewBag.Title));
294+
295+ if (dataContext.FieldValueInitializers.Count > 0) {
296+ pageControlScripts.AppendFormat(@"
297+
298+ $(viewModel).on(""newRecord"", function(event, newRecord) {{
299+ // Set initial field values");
300+
301+ foreach (Tuple<string, string> initialValue in dataContext.FieldValueInitializers) {
302+ pageControlScripts.AppendFormat("\r\n newRecord.{0} = {1};",
303+ initialValue.Item1, initialValue.Item2);
304+ }
305+
306+ pageControlScripts.Append("\r\n });");
307+ }
308+
309+ if (dataContext.DefinedDateFields.Count > 0) {
310+ pageControlScripts.AppendFormat(@"
311+
312+ $(viewModel).on(""derivingObservableRecord"", function(event, observableRecord) {{
313+ // Convert date objects to strings");
314+
315+ foreach (string dateField in dataContext.DefinedDateFields) {
316+ pageControlScripts.AppendFormat("\r\n observableRecord.{0}(notNull(observableRecord.{0}()).formatDate(DateFormat));", dateField);
317+ }
318+
319+ pageControlScripts.Append("\r\n });");
320+
321+ pageControlScripts.AppendFormat(@"
322+
323+ $(viewModel).on(""derivingJSRecord"", function(event, observableRecord) {{
324+ // Convert date strings back to date objects before serialization");
325+
326+ foreach (string dateField in dataContext.DefinedDateFields) {
327+ pageControlScripts.AppendFormat("\r\n observableRecord.{0}(observableRecord.{0}().toDate());", dateField);
328+ }
329+
330+ pageControlScripts.Append("\r\n });");
331+ }
332+
333+ if (dataContext.FieldValidationParameters.Count > 0) {
334+ pageControlScripts.Append("\r\n\r\n viewModel.setApplyValidationParameters(function () {");
335+
336+ foreach (KeyValuePair<string, Tuple<string, string>> fieldValidation in dataContext.FieldValidationParameters) {
337+ pageControlScripts.AppendFormat("\r\n {0}.extend({{ pattern: {{ message: \"{1}\", params: \"{2}\" }} }});",
338+ fieldValidation.Key,
339+ fieldValidation.Value.Item2.JavaScriptEncode(),
340+ fieldValidation.Value.Item1.JavaScriptEncode());
341+ }
342+
343+ pageControlScripts.Append("\r\n });");
344+ }
345+
346+ pageControlScripts.Append(@"
347+ </script>");
348+}
Source/Libraries/GSF.Web/Security/AuthorizationCache.cs+39−0
@@ -0,0 +1,39 @@
1+//******************************************************************************************************
2+// AuthorizationCache.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/11/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Concurrent;
26+
27+namespace GSF.Web.Security
28+{
29+ /// <summary>
30+ /// Defines authorization cache.
31+ /// </summary>
32+ public class AuthorizationCache
33+ {
34+ /// <summary>
35+ /// Gets the current user ID cache.
36+ /// </summary>
37+ public static readonly ConcurrentDictionary<string, Guid> UserIDs = new ConcurrentDictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
38+ }
39+}
Source/Libraries/GSF.Web/Security/AuthorizeControllerRoleAttribute.cs+156−0
@@ -0,0 +1,156 @@
1+//******************************************************************************************************
2+// RoleBasedSecurityAttribute.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/23/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Linq;
26+using System.Security;
27+using System.Threading;
28+using System.Web.Mvc;
29+using GSF.Collections;
30+using GSF.Data;
31+using GSF.Identity;
32+using GSF.Security;
33+
34+namespace GSF.Web.Security
35+{
36+ /// <summary>
37+ /// Defines an MVC filter attribute to handle the GSF role based security model.
38+ /// </summary>
39+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
40+ public class AuthorizeControllerRoleAttribute : FilterAttribute, IAuthorizationFilter, IExceptionFilter
41+ {
42+ #region [ Members ]
43+
44+ // Fields
45+
46+ /// <summary>
47+ /// Gets allowed roles as a string array.
48+ /// </summary>
49+ public readonly string[] AllowedRoles;
50+
51+ #endregion
52+
53+ #region [ Constructors ]
54+
55+ /// <summary>
56+ /// Creates a new MVC based authorization attribute.
57+ /// </summary>
58+ public AuthorizeControllerRoleAttribute()
59+ {
60+ AllowedRoles = new string[0];
61+ SettingsCategory = "securityProvider";
62+ SecurityExceptionViewName = "SecurityError";
63+ }
64+
65+ /// <summary>
66+ /// Creates a new MVC based authorization attribute for specified roles.
67+ /// </summary>
68+ public AuthorizeControllerRoleAttribute(string allowedRoles)
69+ {
70+ AllowedRoles = allowedRoles?.Split(',').Select(role => role.Trim()).
71+ Where(role => !string.IsNullOrEmpty(role)).ToArray() ?? new string[0];
72+
73+ SettingsCategory = "securityProvider";
74+ SecurityExceptionViewName = "SecurityError";
75+ }
76+
77+ #endregion
78+
79+ #region [ Properties ]
80+
81+ /// <summary>
82+ /// Gets or sets settings category to use for loading data context for security info.
83+ /// </summary>
84+ public string SettingsCategory
85+ {
86+ get; set;
87+ }
88+
89+ /// <summary>
90+ /// Gets or sets view name to show for security exceptions.
91+ /// </summary>
92+ public string SecurityExceptionViewName
93+ {
94+ get; set;
95+ }
96+
97+ #endregion
98+
99+ #region [ Methods ]
100+
101+ /// <summary>
102+ /// Called when authorization is required.
103+ /// </summary>
104+ /// <param name="filterContext">The filter context.</param>
105+ public void OnAuthorization(AuthorizationContext filterContext)
106+ {
107+ // Initialize the security principal from caller's windows identity if uninitialized.
108+ if (SecurityProviderCache.CurrentProvider == null)
109+ SecurityProviderCache.CurrentProvider = SecurityProviderUtility.CreateProvider(string.Empty);
110+
111+ // Setup the principal
112+ filterContext.HttpContext.User = Thread.CurrentPrincipal;
113+
114+ // Get current user name
115+ string userName = Thread.CurrentPrincipal.Identity.Name;
116+
117+ // Verify that the current thread principal has been authenticated.
118+ if (!Thread.CurrentPrincipal.Identity.IsAuthenticated)
119+ throw new SecurityException($"Authentication failed for user '{userName}': {SecurityProviderCache.CurrentProvider.AuthenticationFailureReason}");
120+
121+ if (AllowedRoles.Length > 0 && !AllowedRoles.Any(role => filterContext.HttpContext.User.IsInRole(role)))
122+ throw new SecurityException($"Access is denied for user '{userName}' defined: minimum required roles = {AllowedRoles.ToDelimitedString(", ")}.");
123+
124+ // Make sure current user ID is cached
125+ if (!AuthorizationCache.UserIDs.ContainsKey(userName))
126+ {
127+ using (AdoDataConnection connection = new AdoDataConnection(SettingsCategory))
128+ {
129+ AuthorizationCache.UserIDs.TryAdd(userName, connection.ExecuteScalar<Guid?>("SELECT ID FROM UserAccount WHERE Name={0}", UserInfo.UserNameToSID(userName)) ?? Guid.Empty);
130+ }
131+ }
132+ }
133+
134+ /// <summary>
135+ /// Called when an exception occurs.
136+ /// </summary>
137+ /// <param name="filterContext">The filter context.</param>
138+ public void OnException(ExceptionContext filterContext)
139+ {
140+ if (filterContext.Exception is SecurityException)
141+ {
142+ filterContext.Result = new ViewResult
143+ {
144+ ViewName = SecurityExceptionViewName,
145+ ViewData = new ViewDataDictionary
146+ {
147+ ["Exception"] = filterContext.Exception
148+ }
149+ };
150+ filterContext.ExceptionHandled = true;
151+ }
152+ }
153+
154+ #endregion
155+ }
156+}
Source/Libraries/GSF.Web/Security/AuthorizeHubRoleAttribute.cs+138−0
@@ -0,0 +1,138 @@
1+//******************************************************************************************************
2+// AuthorizeHubRole.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/25/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Linq;
26+using System.Security;
27+using System.Security.Principal;
28+using System.Threading;
29+using GSF.Collections;
30+using GSF.Data;
31+using GSF.Identity;
32+using GSF.Security;
33+using Microsoft.AspNet.SignalR;
34+
35+namespace GSF.Web.Security
36+{
37+ /// <summary>
38+ /// Defines a SignalR authorization attribute to handle the GSF role based security model.
39+ /// </summary>
40+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
41+ public class AuthorizeHubRoleAttribute : AuthorizeAttribute
42+ {
43+ #region [ Members ]
44+
45+ // Fields
46+ private string[] m_allowedRoles;
47+
48+ #endregion
49+
50+ #region [ Constructors ]
51+
52+ /// <summary>
53+ /// Creates a new <see cref="AuthorizeHubRoleAttribute"/>.
54+ /// </summary>
55+ public AuthorizeHubRoleAttribute()
56+ {
57+ SettingsCategory = "securityProvider";
58+ }
59+
60+ /// <summary>
61+ /// Creates a new <see cref="AuthorizeHubRoleAttribute"/> with specified allowed roles.
62+ /// </summary>
63+ public AuthorizeHubRoleAttribute(string allowedRoles)
64+ {
65+ Roles = allowedRoles;
66+ SettingsCategory = "securityProvider";
67+ }
68+
69+ #endregion
70+
71+ #region [ Properties ]
72+
73+ /// <summary>
74+ /// Gets or sets settings category to use for loading data context for security info.
75+ /// </summary>
76+ public string SettingsCategory
77+ {
78+ get; set;
79+ }
80+
81+ /// <summary>
82+ /// Gets the allowed <see cref="AuthorizeAttribute.Roles"/> as a string array.
83+ /// </summary>
84+ public string[] AllowedRoles => m_allowedRoles ?? (m_allowedRoles = Roles?.Split(',').Select(role => role.Trim()).Where(role => !string.IsNullOrEmpty(role)).ToArray() ?? new string[0]);
85+
86+ #endregion
87+
88+ #region [ Methods ]
89+
90+ /// <summary>
91+ /// Provides an entry point for custom authorization checks.
92+ /// </summary>
93+ /// <param name="user">The <see cref="IPrincipal"/> for the client being authorize</param>
94+ /// <returns>
95+ /// <c>true</c> if the user is authorized, otherwise, <c>false</c>.
96+ /// </returns>
97+ protected override bool UserAuthorized(IPrincipal user)
98+ {
99+ // Get current user name
100+ string userName = user.Identity.Name;
101+
102+ // Initialize the security principal from caller's windows identity if uninitialized, note that
103+ // simply by checking current provider any existing cached security principal will be restored
104+ if (SecurityProviderCache.CurrentProvider == null)
105+ {
106+ lock (typeof(AuthorizeHubRoleAttribute))
107+ {
108+ // Let's see if we won the race...
109+ if (SecurityProviderCache.CurrentProvider == null)
110+ SecurityProviderCache.CurrentProvider = SecurityProviderUtility.CreateProvider(userName);
111+ }
112+ }
113+
114+ // Setup the principal
115+ user = Thread.CurrentPrincipal;
116+
117+ // Verify that the current thread principal has been authenticated.
118+ if (!Thread.CurrentPrincipal.Identity.IsAuthenticated)
119+ throw new SecurityException($"Authentication failed for user '{userName}': {SecurityProviderCache.CurrentProvider.AuthenticationFailureReason}");
120+
121+ if (AllowedRoles.Length > 0 && !AllowedRoles.Any(role => user.IsInRole(role)))
122+ throw new SecurityException($"Access is denied for user '{userName}' defined: minimum required roles = {AllowedRoles.ToDelimitedString(", ")}.");
123+
124+ // Make sure current user ID is cached
125+ if (!AuthorizationCache.UserIDs.ContainsKey(userName))
126+ {
127+ using (AdoDataConnection connection = new AdoDataConnection(SettingsCategory))
128+ {
129+ AuthorizationCache.UserIDs.TryAdd(userName, connection.ExecuteScalar<Guid?>("SELECT ID FROM UserAccount WHERE Name={0}", UserInfo.UserNameToSID(userName)) ?? Guid.Empty);
130+ }
131+ }
132+
133+ return true;
134+ }
135+
136+ #endregion
137+ }
138+}
Source/Libraries/GSF.Web/Security/SecurityHub.cs+292−0
@@ -0,0 +1,292 @@
1+//******************************************************************************************************
2+// SecurityHub.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/03/2016 - Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Generic;
26+using GSF.Configuration;
27+using GSF.Data;
28+using GSF.Data.Model;
29+using GSF.Identity;
30+using GSF.Security.Model;
31+using GSF.Web.Model;
32+using Microsoft.AspNet.SignalR;
33+using Microsoft.AspNet.SignalR.Hubs;
34+
35+namespace GSF.Web.Security
36+{
37+ /// <summary>
38+ /// Defines a SignalR security hub for managing users, groups and SID management.
39+ /// </summary>
40+ [AuthorizeHubRole]
41+ public class SecurityHub : Hub
42+ {
43+ #region [ Members ]
44+
45+ // Fields
46+ private readonly DataContext m_dataContext;
47+ private bool m_disposed;
48+
49+ #endregion
50+
51+ #region [ Constructors ]
52+
53+ /// <summary>
54+ /// Creates a new <see cref="SecurityHub"/>.
55+ /// </summary>
56+ public SecurityHub()
57+ {
58+ m_dataContext = new DataContext();
59+ }
60+
61+ #endregion
62+
63+ #region [ Methods ]
64+
65+ /// <summary>
66+ /// Releases the unmanaged resources used by the <see cref="SecurityHub"/> object and optionally releases the managed resources.
67+ /// </summary>
68+ /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
69+ protected override void Dispose(bool disposing)
70+ {
71+ if (!m_disposed)
72+ {
73+ try
74+ {
75+ if (disposing)
76+ m_dataContext?.Dispose();
77+ }
78+ finally
79+ {
80+ m_disposed = true; // Prevent duplicate dispose.
81+ base.Dispose(disposing); // Call base class Dispose().
82+ }
83+ }
84+ }
85+
86+ #endregion
87+
88+ // Client-side script functionality
89+
90+ /// <summary>
91+ /// Gets the specified user account record.
92+ /// </summary>
93+ /// <param name="id">ID of requested user.</param>
94+ /// <returns>Specified user account record.</returns>
95+ public UserAccount QueryUserAccount(Guid id)
96+ {
97+ return m_dataContext.Table<UserAccount>().LoadRecord(id);
98+ }
99+
100+ /// <summary>
101+ /// Gets the current application role records.
102+ /// </summary>
103+ /// <returns>Current application role records.</returns>
104+ public IEnumerable<ApplicationRole> QueryApplicationRoles()
105+ {
106+ return m_dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", DefaultNodeID));
107+ }
108+
109+ /// <summary>
110+ /// Determines if user is in role based on database ID values.
111+ /// </summary>
112+ /// <param name="userID">User ID value.</param>
113+ /// <param name="roleID">Role ID value.</param>
114+ /// <returns><c>true</c> if user is in role; otherwise, <c>false</c>.</returns>
115+ public bool UserIsInRole(Guid userID, Guid roleID)
116+ {
117+ return m_dataContext.Table<ApplicationRoleUserAccount>().QueryRecordCount(new RecordRestriction("UserAccountID={0} AND ApplicationRoleID={1}", userID, roleID)) > 0;
118+ }
119+
120+ /// <summary>
121+ /// Adds user to a role.
122+ /// </summary>
123+ /// <param name="userID">User ID value.</param>
124+ /// <param name="roleID">Role ID value.</param>
125+ /// <returns><c>true</c> if user was added; otherwise <c>false</c>.</returns>
126+ [AuthorizeHubRole("Administrator")]
127+ public bool AddUserToRole(Guid userID, Guid roleID)
128+ {
129+ // Nothing to do if user is already in role
130+ if (UserIsInRole(userID, roleID))
131+ return false;
132+
133+ return m_dataContext.Table<ApplicationRoleUserAccount>().AddNewRecord(new ApplicationRoleUserAccount
134+ {
135+ ApplicationRoleID = roleID,
136+ UserAccountID = userID
137+ }) > 0;
138+ }
139+
140+ /// <summary>
141+ /// Removes user from a role.
142+ /// </summary>
143+ /// <param name="userID">User ID value.</param>
144+ /// <param name="roleID">Role ID value.</param>
145+ /// <returns><c>true</c> if user was removed; otherwise <c>false</c>.</returns>
146+ [AuthorizeHubRole("Administrator")]
147+ public bool RemoveUserFromRole(Guid userID, Guid roleID)
148+ {
149+ // Nothing to do if user is not currently in role
150+ if (!UserIsInRole(userID, roleID))
151+ return false;
152+
153+ return m_dataContext.Table<ApplicationRoleUserAccount>().DeleteRecord(new RecordRestriction("UserAccountID={0} AND ApplicationRoleID={1}", userID, roleID)) > 0;
154+ }
155+
156+ /// <summary>
157+ /// Gets SID for a given user name.
158+ /// </summary>
159+ /// <param name="userName">User name to convert to SID.</param>
160+ /// <returns>SID for a given user name.</returns>
161+ public string UserNameToSID(string userName)
162+ {
163+ return UserInfo.UserNameToSID(userName);
164+ }
165+
166+ /// <summary>
167+ /// Determines if SID is for a user.
168+ /// </summary>
169+ /// <param name="sid">Security identifier to test.</param>
170+ /// <returns><c>true</c>if <paramref name="sid"/> is for a user; otherwise, <c>false</c>.</returns>
171+ [HubMethodName("isUserSID")]
172+ public bool IsUserSID(string sid)
173+ {
174+ return UserInfo.IsUserSID(sid);
175+ }
176+
177+ /// <summary>
178+ /// Determines if group is in role based on database ID values.
179+ /// </summary>
180+ /// <param name="groupID">Group ID value.</param>
181+ /// <param name="roleID">Role ID value.</param>
182+ /// <returns><c>true</c> if group is in role; otherwise, <c>false</c>.</returns>
183+ public bool GroupIsInRole(Guid groupID, Guid roleID)
184+ {
185+ return m_dataContext.Table<ApplicationRoleSecurityGroup>().QueryRecordCount(new RecordRestriction("SecurityGroupID={0} AND ApplicationRoleID={1}", groupID, roleID)) > 0;
186+ }
187+
188+ /// <summary>
189+ /// Adds group to a role.
190+ /// </summary>
191+ /// <param name="groupID">Group ID value.</param>
192+ /// <param name="roleID">Role ID value.</param>
193+ /// <returns><c>true</c> if group was added; otherwise <c>false</c>.</returns>
194+ [AuthorizeHubRole("Administrator")]
195+ public bool AddGroupToRole(Guid groupID, Guid roleID)
196+ {
197+ // Nothing to do if group is already in role
198+ if (GroupIsInRole(groupID, roleID))
199+ return false;
200+
201+ return m_dataContext.Table<ApplicationRoleSecurityGroup>().AddNewRecord(new ApplicationRoleSecurityGroup
202+ {
203+ ApplicationRoleID = roleID,
204+ SecurityGroupID = groupID
205+ }) > 0;
206+ }
207+
208+ /// <summary>
209+ /// Removes group from a role.
210+ /// </summary>
211+ /// <param name="groupID">Group ID value.</param>
212+ /// <param name="roleID">Role ID value.</param>
213+ /// <returns><c>true</c> if group was removed; otherwise <c>false</c>.</returns>
214+ [AuthorizeHubRole("Administrator")]
215+ public bool RemoveGroupFromRole(Guid groupID, Guid roleID)
216+ {
217+ // Nothing to do if group is not currently in role
218+ if (!GroupIsInRole(groupID, roleID))
219+ return false;
220+
221+ return m_dataContext.Table<ApplicationRoleSecurityGroup>().DeleteRecord(new RecordRestriction("SecurityGroupID={0} AND ApplicationRoleID={1}", groupID, roleID)) > 0;
222+ }
223+
224+ /// <summary>
225+ /// Gets SID for a given group name.
226+ /// </summary>
227+ /// <param name="groupName">Group name to convert to SID.</param>
228+ /// <returns>SID for a given group name.</returns>
229+ public string GroupNameToSID(string groupName)
230+ {
231+ return UserInfo.GroupNameToSID(groupName ?? "");
232+ }
233+
234+ /// <summary>
235+ /// Determines if SID is for a group.
236+ /// </summary>
237+ /// <param name="sid">Security identifier to test.</param>
238+ /// <returns><c>true</c>if <paramref name="sid"/> is for a group; otherwise, <c>false</c>.</returns>
239+ [HubMethodName("isGroupSID")]
240+ public bool IsGroupSID(string sid)
241+ {
242+ return UserInfo.IsGroupSID(sid);
243+ }
244+
245+ /// <summary>
246+ /// Gets account name for a given SID.
247+ /// </summary>
248+ /// <param name="sid">SID to convert to a account name.</param>
249+ /// <returns>Account name for a given SID.</returns>
250+ [HubMethodName("sidToAccountName")]
251+ public string SIDToAccountName(string sid)
252+ {
253+ return UserInfo.SIDToAccountName(sid ?? "");
254+ }
255+
256+ #region [ Static ]
257+
258+ // Static Fields
259+
260+ /// <summary>
261+ /// Gets current default Node ID for security.
262+ /// </summary>
263+ public static readonly Guid DefaultNodeID;
264+
265+ // Static Constructor
266+ static SecurityHub()
267+ {
268+ CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
269+
270+ // Retrieve default NodeID
271+ DefaultNodeID = Guid.Parse(systemSettings["NodeID"].Value.ToNonNullString(Guid.NewGuid().ToString()));
272+
273+ // Determine whether the node exists in the database and create it if it doesn't
274+ using (AdoDataConnection connection = new AdoDataConnection("systemSettings"))
275+ {
276+ const string NodeCountFormat = "SELECT COUNT(*) FROM Node";
277+ const string NodeInsertFormat = "INSERT INTO Node(Name, Description, Enabled) VALUES('Default', 'Default node', 1)";
278+ const string NodeUpdateFormat = "UPDATE Node SET ID = {0}";
279+
280+ int nodeCount = connection.ExecuteScalar<int?>(NodeCountFormat) ?? 0;
281+
282+ if (nodeCount == 0)
283+ {
284+ connection.ExecuteNonQuery(NodeInsertFormat);
285+ connection.ExecuteNonQuery(NodeUpdateFormat, connection.Guid(DefaultNodeID));
286+ }
287+ }
288+ }
289+
290+ #endregion
291+ }
292+}
Source/Libraries/GSF.Web/Security/Views/Groups.cshtml+370−0
@@ -0,0 +1,370 @@
1+@*******************************************************************************************************
2+// Groups.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/03/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using GSF.Data.Model
24+@using GSF.Security.Model
25+@using GSF.Web.Model
26+@section StyleSheets{
27+ <style>
28+ .account-feedback-wrapper {
29+ position: relative;
30+ display: table
31+ }
32+
33+ .account-feedback-message {
34+ position: absolute;
35+ vertical-align: middle;
36+ display: table-cell;
37+ left: 10px;
38+ width: 425px;
39+ display: none
40+ }
41+
42+ .account-feedback-icon {
43+ font-size: large;
44+ vertical-align: middle;
45+ margin-top: -5px
46+ }
47+ </style>
48+}
49+@{
50+ DataContext dataContext = ViewBag.DataContext;
51+
52+ // Define column headers, use null for field name to make column non-sortable
53+ ViewBag.HeaderColumns = new[]
54+ { // { "Field", "Label", "Classes" }
55+ new[] { null, "Group Name", "text-center valign-middle" },
56+ new[] { null, "Is&nbsp;AD&nbsp;Group", "text-center" },
57+ new[] { "CreatedOn", "Date&nbsp;Added", "text-center" },
58+ new[] { null, "Group Roles", "text-center valign-middle" }
59+ };
60+
61+ ViewBag.BodyRows = BodyRows().ToString();
62+ ViewBag.AddNewEditDialog = AddNewEditDialog(dataContext).ToString();
63+}
64+@helper BodyRows()
65+{
66+ <td width="65%" class="text-center valign-middle" nowrap><button type="button" class="btn btn-link" data-bind="text: AccountName(), attr: {title: Name}, click: $parent.viewPageRecord"></button></td>
67+ <td width="10%" class="text-center valign-middle"><input type="checkbox" id="checkEnabled" data-bind="checked: useADAuthentication(AccountName())" disabled></td>
68+ <td width="10%" class="text-center valign-middle" data-bind="text: CreatedOn.formatDate(DateFormat)"></td>
69+ <td width="10%" class="text-center valign-middle"><button type="button" class="btn btn-default btn-sm" data-bind="click: openRolesEditor.bind($data), enable: $parent.canEdit() && $parent.dataHubIsConnected()">Roles...</button></td>
70+ <td width="5%" class="text-center valign-middle" nowrap>
71+ <button type="button" class="btn btn-xs" data-bind="click: $parent.editPageRecord, enable: $parent.canEdit() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-pencil"></span></button>
72+ <button type="button" class="btn btn-xs" data-bind="click: $parent.removePageRecord, enable: $parent.canDelete() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-remove"></span></button>
73+ </td>
74+}
75+@helper AccountValidationFeedback()
76+{
77+ <div class="pull-right account-feedback-wrapper">
78+ <span id="resolvingAccount" class="account-feedback-message"><span class="glyphicon glyphicon-refresh glyphicon-spin account-feedback-icon"></span>&nbsp;<em class="small">Resolving account details...</em></span>
79+ <span id="accountValid" class="account-feedback-message"><span class="glyphicon glyphicon-ok-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'green'}"></span>&nbsp;<em class="small">Resolved account name</em></span>
80+ <span id="accountInvalid" class="account-feedback-message"><span class="glyphicon glyphicon-remove-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'red'}"></span>&nbsp;<em class="small">Cannot resolve account name</em></span>
81+ <span id="accountUnknown" class="account-feedback-message"><span class="glyphicon glyphicon-warning-sign account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'orange'}"></span>&nbsp;<em class="small">Valid account name is not a group</em></span>
82+ </div>
83+}
84+@helper AddNewEditDialog(DataContext dataContext)
85+{
86+ <div class="col-md-12">
87+ @Html.Raw(dataContext.AddInputField<SecurityGroup>("Name", fieldLabel: $"Name{AccountValidationFeedback().ToString().Trim()}", customDataBinding: "attr: {'acct-valid': validateAccount($data)}", initialFocus: true))
88+ <div class="panel panel-default" data-bind="css: {'panel-readonly': $root.recordMode()===RecordMode.View, 'panel-primary': UseDBAuthentication}">
89+ <div class="panel-heading">
90+ <div class="row">
91+ <div class="col-md-8">
92+ <div class="row">
93+ <div class="col-md-4">
94+ <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: false, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}">&nbsp;Active&nbsp;Directory&nbsp;Group</label>
95+ </div>
96+ <div class="col-md-8">
97+ <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: true, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}">&nbsp;Database&nbsp;Group</label>
98+ </div>
99+ </div>
100+ </div>
101+ <div class="col-md-4">
102+ </div>
103+ </div>
104+ </div>
105+ <div class="panel-body" data-bind="visible: UseDBAuthentication">
106+ @Html.Raw(dataContext.AddTextAreaField<SecurityGroup>("Description", 4, dependencyFieldName: "UseDBAuthentication"))
107+ </div>
108+ </div>
109+ </div>
110+}
111+@Html.Partial("PagedViewModel")
112+<div id="editRolesDialog" class="modal fade" role="dialog">
113+ <div class="modal-dialog">
114+ <div class="modal-content">
115+ <div class="modal-header">
116+ <button type="button" class="close" data-dismiss="modal">&times;</button>
117+ <h4 class="modal-title">Edit Roles for <span id="currentGroupName"></span>:</h4>
118+ </div>
119+ <div class="modal-body">
120+ <div class="modal-body auto-height text-center">
121+ <form role="form">
122+ <input type="hidden" id="currentGroupID" />
123+ <div class="center-block">
124+ <div id="loadingRoles">Loading role assignments&nbsp;&nbsp;<span class="glyphicon glyphicon-refresh glyphicon-spin"></span><hr class="full-break" /></div>
125+ <table class="table-condensed table-center">
126+ @foreach (ApplicationRole role in dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", Model.Global.NodeID))) {
127+ <tr>
128+ <td class="text-left valign-middle">
129+ <div class="checkbox" style="margin: 0">
130+ <label for="@role.ID"><input type="checkbox" id="@role.ID"> @role.Description (@role.Name)</label>
131+ </div>
132+ </td>
133+ </tr>
134+ }
135+ </table>
136+ <br />
137+ <div class="text-center">
138+ <a href="@Url.Action("Help", "Main")#Roles" target="_blank">@Model.Global.ApplicationName Role Definitions</a>
139+ </div>
140+ </div>
141+ </form>
142+ </div>
143+ </div>
144+ <div class="modal-footer">
145+ <button type="submit" class="btn btn-primary" data-dismiss="modal" id="saveRolesButton" data-bind="enable: canEdit()">Save</button>
146+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
147+ </div>
148+ </div>
149+ </div>
150+</div>
151+@section Scripts {
152+ <script>
153+ "use strict";
154+
155+ @Html.Raw(dataContext.RenderViewModelConfiguration<SecurityGroup>(ViewBag))
156+ var AccountState = {
157+ Invalid: 0,
158+ Valid: 1,
159+ Unknown: 2,
160+ Resolving: 3
161+ }
162+
163+ function lookupAccountName(record) {
164+ if (viewModel.dataHubIsConnected()) {
165+ securityHub.sidToAccountName(record.Name).done(function (accountName) {
166+ record.AccountName(accountName);
167+ });
168+ }
169+ }
170+
171+ function validateAccount(observableRecord) {
172+ showAccountNameValidityFeedback(AccountState.Resolving);
173+
174+ if (observableRecord.UseDBAuthentication == null || observableRecord.UseDBAuthentication()) {
175+ hideAccountNameValidityFeedback();
176+ }
177+ else {
178+ if (viewModel.dataHubIsConnected()) {
179+ const accountName = observableRecord.Name();
180+
181+ if (isEmpty(accountName)) {
182+ showAccountNameValidityFeedback(AccountState.Invalid);
183+ } else {
184+ securityHub.groupNameToSID(accountName).done(function (sid) {
185+ if (accountName !== sid && accountName.countOccurrences("\\") < 2) {
186+ if (accountName.startsWith("NT AUTHORITY")) {
187+ // This is a valid group source for things like "Authenticated Users", but is
188+ // not technically a "group" - since SID validated, we report a valid account
189+ showAccountNameValidityFeedback(AccountState.Valid);
190+ } else {
191+ securityHub.isGroupSID(sid).done(function (isGroup) {
192+ showAccountNameValidityFeedback(isGroup ? AccountState.Valid : AccountState.Unknown);
193+ });
194+ }
195+ } else {
196+ showAccountNameValidityFeedback(AccountState.Invalid);
197+ }
198+ });
199+ }
200+ }
201+ else {
202+ hideAccountNameValidityFeedback();
203+ }
204+ }
205+
206+ return undefined;
207+ }
208+
209+ var messages = [];
210+ var nextMessageName = undefined;
211+ var showingMessage = false;
212+ messages.push("resolvingAccount");
213+ messages.push("accountValid");
214+ messages.push("accountInvalid");
215+ messages.push("accountUnknown");
216+
217+ function showFeedbackMessage(messageName) {
218+ if (showingMessage) {
219+ nextMessageName = messageName;
220+ } else {
221+ showingMessage = true;
222+
223+ hideAccountNameValidityFeedback(messageName);
224+
225+ if (messageName == null)
226+ showingMessage = false;
227+ else
228+ $("#" + messageName).fadeIn(function () {
229+ showingMessage = false;
230+
231+ if (nextMessageName != undefined) {
232+ messageName = nextMessageName;
233+ nextMessageName = undefined;
234+ showFeedbackMessage(messageName);
235+ }
236+ });
237+ }
238+ }
239+
240+ function hideAccountNameValidityFeedback(messageName) {
241+ for (let i = 0; i < messages.length; i++) {
242+ if (messages[i] !== messageName)
243+ $("#" + messages[i]).hide();
244+ }
245+ }
246+
247+ function showAccountNameValidityFeedback(accountState) {
248+ switch (accountState) {
249+ case AccountState.Valid:
250+ showFeedbackMessage("accountValid");
251+ break;
252+ case AccountState.Invalid:
253+ showFeedbackMessage("accountInvalid");
254+ break;
255+ case AccountState.Unknown:
256+ showFeedbackMessage("accountUnknown");
257+ break;
258+ case AccountState.Resolving:
259+ showFeedbackMessage("resolvingAccount");
260+ break;
261+ }
262+ }
263+
264+ function useADAuthentication(accountName)
265+ {
266+ const groupName = notNull(accountName);
267+
268+ // Just deriving if group is AD based by delimiters in group name
269+ if (groupName.length > 0)
270+ return (groupName.indexOf("\\") > 0 || groupName.indexOf("@@") > 0 || groupName.startsWith("S-"));
271+
272+ return true; // Default to AD group
273+ }
274+
275+ $(viewModel).on("pageRecordsQueried", function (event, records) {
276+ // Add a virtual observable field called "AccountName" that will resolve SID values to account names
277+ for (var i = 0; i < records.length; i++) {
278+ records[i].AccountName = ko.observable("...");
279+ lookupAccountName(records[i]);
280+ }
281+ });
282+
283+ $(viewModel).on("newRecord", function (event, newRecord) {
284+ // Make sure new records get virtual fields
285+ newRecord.AccountName = ko.observable("");
286+ });
287+
288+ $(viewModel).on("derivingObservableRecord", function (event, observableRecord) {
289+ // Load human readable account name into Name field
290+ if (observableRecord.AccountName)
291+ observableRecord.Name(observableRecord.AccountName());
292+
293+ // Create a virtual UseDBAuthentication field
294+ observableRecord.UseDBAuthentication = ko.observable(!useADAuthentication(notNull(observableRecord.Name())));
295+ });
296+
297+ $(viewModel).on("derivingJSRecord", function (event, observableRecord, promises) {
298+ const isADGroup = !observableRecord.UseDBAuthentication();
299+
300+ // Remove virtual observable fields before serialization for record updates
301+ delete observableRecord.UseDBAuthentication;
302+ delete observableRecord.AccountName;
303+
304+ // Push promise to convert group name back to SID before updating record
305+ if (viewModel.dataHubIsConnected() && isADGroup) {
306+ promises.push($.Deferred(function (deferred) {
307+ securityHub.groupNameToSID(observableRecord.Name()).done(function (sid) {
308+ observableRecord.Name(sid);
309+ deferred.resolve();
310+ });
311+ }).promise());
312+ }
313+ });
314+
315+ function openRolesEditor(record) {
316+ $("#currentGroupID").text(record.ID);
317+ $("#currentGroupName").text(record.AccountName());
318+ $("#editRolesDialog input:checkbox").prop("checked", false);
319+
320+ if (viewModel.dataHubIsConnected()) {
321+ securityHub.queryApplicationRoles().done(function (roles) {
322+ var evaluations = roles.length;
323+
324+ if (evaluations > 0)
325+ $("#loadingRoles").show();
326+
327+ for (var i = 0; i < roles.length; i++) {
328+ securityHub.groupIsInRole(record.ID, roles[i].ID).done(function (roleID) {
329+ return function (result) {
330+ $("#" + roleID).prop("checked", result);
331+
332+ evaluations--;
333+
334+ if (evaluations <= 0)
335+ $("#loadingRoles").hide();
336+ };
337+ }(roles[i].ID));
338+ }
339+ $("#editRolesDialog").modal("show");
340+ });
341+ }
342+ }
343+
344+ $(function () {
345+ $("#editRolesDialog").modal({ show: false, backdrop: "static" });
346+
347+ $("#saveRolesButton").click(function () {
348+ if (viewModel.dataHubIsConnected()) {
349+ const groupID = $("#currentGroupID").text();
350+ const roles = $("#editRolesDialog input:checkbox");
351+
352+ for (var i = 0; i < roles.length; i++) {
353+ const element = $(roles[i]);
354+ const roleID = element.attr("id");
355+
356+ if (element.prop("checked")) {
357+ securityHub.addGroupToRole(groupID, roleID).fail(function (error) {
358+ showErrorMessage(error);
359+ });
360+ } else {
361+ securityHub.removeGroupFromRole(groupID, roleID).fail(function (error) {
362+ showErrorMessage(error);
363+ });
364+ }
365+ }
366+ }
367+ });
368+ });
369+ </script>
370+}
Source/Libraries/GSF.Web/Security/Views/Users.cshtml+381−0
@@ -0,0 +1,381 @@
1+@*******************************************************************************************************
2+// Users.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/03/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using GSF.Data.Model
24+@using GSF.Security.Model
25+@using GSF.Web.Model
26+@section StyleSheets{
27+ <style>
28+ .account-feedback-wrapper {
29+ position: relative;
30+ display: table
31+ }
32+
33+ .account-feedback-message {
34+ position: absolute;
35+ vertical-align: middle;
36+ display: table-cell;
37+ left: 10px;
38+ width: 425px;
39+ display: none
40+ }
41+
42+ .account-feedback-icon {
43+ font-size: large;
44+ vertical-align: middle;
45+ margin-top: -5px
46+ }
47+ </style>
48+}
49+@{
50+ DataContext dataContext = Model.DataContext;
51+
52+ // Define column headers, use null for field name to make column non-sortable
53+ ViewBag.HeaderColumns = new[]
54+ { // { "Field", "Label", "Classes" }
55+ new[] { null, "User Name", "text-center valign-middle" },
56+ new[] { "UseADAuthentication", "Is&nbsp;AD&nbsp;User", "text-center" },
57+ new[] { "CreatedOn", "Date&nbsp;Added", "text-center" },
58+ new[] { null, "User Roles", "text-center valign-middle" }
59+ };
60+
61+ ViewBag.BodyRows = BodyRows().ToString();
62+ ViewBag.AddNewEditDialog = AddNewEditDialog(dataContext).ToString();
63+}
64+@helper BodyRows()
65+{
66+ <td width="65%" class="text-center valign-middle" nowrap><button type="button" class="btn btn-link" data-bind="text: AccountName(), attr: {title: Name}, click: $parent.viewPageRecord"></button></td>
67+ <td width="10%" class="text-center valign-middle"><input type="checkbox" id="checkEnabled" data-bind="checked: UseADAuthentication" disabled></td>
68+ <td width="10%" class="text-center valign-middle" data-bind="text: CreatedOn.formatDate(DateFormat)"></td>
69+ <td width="10%" class="text-center valign-middle"><button type="button" class="btn btn-default btn-sm" data-bind="click: openRolesEditor.bind($data), enable: $parent.canEdit() && $parent.dataHubIsConnected()">Roles...</button></td>
70+ <td width="5%" class="text-center valign-middle" nowrap>
71+ <button type="button" class="btn btn-xs" data-bind="click: $parent.editPageRecord, enable: $parent.canEdit() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-pencil"></span></button>
72+ <button type="button" class="btn btn-xs" data-bind="click: $parent.removePageRecord, enable: $parent.canDelete() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-remove"></span></button>
73+ </td>
74+}
75+@helper AccountValidationFeedback()
76+{
77+ <div class="pull-right account-feedback-wrapper">
78+ <span id="resolvingAccount" class="account-feedback-message"><span class="glyphicon glyphicon-refresh glyphicon-spin account-feedback-icon"></span>&nbsp;<em class="small">Resolving account details...</em></span>
79+ <span id="accountValid" class="account-feedback-message"><span class="glyphicon glyphicon-ok-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'green'}"></span>&nbsp;<em class="small">Resolved account name</em></span>
80+ <span id="accountInvalid" class="account-feedback-message"><span class="glyphicon glyphicon-remove-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'red'}"></span>&nbsp;<em class="small">Cannot resolve account name</em></span>
81+ <span id="accountUnknown" class="account-feedback-message"><span class="glyphicon glyphicon-warning-sign account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'orange'}"></span>&nbsp;<em class="small">Valid account name is not a user or Active Directory access is limited</em></span>
82+ </div>
83+}
84+@helper AddNewEditDialog(DataContext dataContext)
85+{
86+ <div class="col-md-12">
87+ @Html.Raw(dataContext.AddInputField<UserAccount>("Name", fieldLabel: $"Name{AccountValidationFeedback().ToString().Trim()}", customDataBinding: "attr: {'acct-valid': validateAccount($data)}", initialFocus: true))
88+ <div class="panel panel-default" data-bind="css: {'panel-readonly': $root.recordMode()===RecordMode.View, 'panel-primary': UseDBAuthentication}">
89+ <div class="panel-heading">
90+ @* Adding actual UseADAuthentication field check-box, but hidden, to pick-up any model based field initializers *@
91+ <div style="display: none">
92+ @Html.Raw(dataContext.AddCheckBoxField<UserAccount>("UseADAuthentication"))
93+ </div>
94+ @* Adding radio buttons bound to virtual UseDBAuthentication that models negated state of UseADAuthentication field for simplified dependency field initialization *@
95+ <div class="row">
96+ <div class="col-md-8">
97+ <div class="row">
98+ <div class="col-md-4">
99+ <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: false, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}">&nbsp;Active&nbsp;Directory&nbsp;Group</label>
100+ </div>
101+ <div class="col-md-8">
102+ <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: true, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}">&nbsp;Database&nbsp;Group</label>
103+ </div>
104+ </div>
105+ </div>
106+ <div class="col-md-4">
107+ </div>
108+ </div>
109+ </div>
110+ <div class="panel-body" data-bind="visible: UseDBAuthentication">
111+ <div class="row">
112+ <div class="col-md-6">
113+ @Html.Raw(dataContext.AddInputField<UserAccount>("Password", "password", dependencyFieldName: "UseDBAuthentication"))
114+ @Html.Raw(dataContext.AddInputField<UserAccount>("FirstName", dependencyFieldName: "UseDBAuthentication"))
115+ @Html.Raw(dataContext.AddInputField<UserAccount>("LastName", dependencyFieldName: "UseDBAuthentication"))
116+ </div>
117+ <div class="col-md-6">
118+ @Html.Raw(dataContext.AddInputField<UserAccount>("Phone", dependencyFieldName: "UseDBAuthentication"))
119+ @Html.Raw(dataContext.AddInputField<UserAccount>("Email", dependencyFieldName: "UseDBAuthentication"))
120+ @Html.Raw(dataContext.AddDateField<UserAccount>("ChangePasswordOn", dependencyFieldName: "UseDBAuthentication"))
121+ @{
122+ Model.AddPageDefinedFieldInitialization("Password", "\"\"",
123+ /* RegEx Pattern: */ Model.Global.PasswordRequirementsRegex,
124+ /* Error Message: */ Model.Global.PasswordRequirementsError);
125+ Model.AddPageDefinedFieldInitialization("ChangePasswordOn", "(new Date()).addDays(90)");
126+ }
127+ </div>
128+ </div>
129+ </div>
130+ </div>
131+ <div class="pull-right">
132+ @Html.Raw(dataContext.AddCheckBoxField<UserAccount>("LockedOut"))
133+ </div>
134+ </div>
135+}
136+@Html.Partial("PagedViewModel")
137+<div id="editRolesDialog" class="modal fade" role="dialog">
138+ <div class="modal-dialog">
139+ <div class="modal-content">
140+ <div class="modal-header">
141+ <button type="button" class="close" data-dismiss="modal">&times;</button>
142+ <h4 class="modal-title">Edit Roles for <span id="currentUserName"></span>:</h4>
143+ </div>
144+ <div class="modal-body">
145+ <div class="modal-body auto-height text-center">
146+ <form role="form">
147+ <input type="hidden" id="currentUserID" />
148+ <div class="center-block">
149+ <div id="loadingRoles">Loading role assignments&nbsp;&nbsp;<span class="glyphicon glyphicon-refresh glyphicon-spin"></span><hr class="full-break" /></div>
150+ <table class="table-condensed table-center">
151+ @foreach (ApplicationRole role in dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", Model.Global.NodeID))) {
152+ <tr>
153+ <td class="text-left valign-middle">
154+ <div class="checkbox" style="margin: 0">
155+ <label for="@role.ID"><input type="checkbox" id="@role.ID"> @role.Description (@role.Name)</label>
156+ </div>
157+ </td>
158+ </tr>
159+ }
160+ </table>
161+ <br />
162+ <div class="text-center">
163+ <a href="@Url.Action("Help", "Main")#Roles" target="_blank">@Model.Global.ApplicationName Role Definitions</a>
164+ </div>
165+ </div>
166+ </form>
167+ </div>
168+ </div>
169+ <div class="modal-footer">
170+ <button type="submit" class="btn btn-primary" data-dismiss="modal" id="saveRolesButton" data-bind="enable: canEdit()">Save</button>
171+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
172+ </div>
173+ </div>
174+ </div>
175+</div>
176+@section Scripts {
177+ <script>
178+ "use strict";
179+
180+ @Html.Raw(dataContext.RenderViewModelConfiguration<UserAccount>(ViewBag))
181+ var AccountState = {
182+ Invalid: 0,
183+ Valid: 1,
184+ Unknown: 2,
185+ Resolving: 3
186+ }
187+
188+ function lookupAccountName(record) {
189+ if (viewModel.dataHubIsConnected()) {
190+ securityHub.sidToAccountName(record.Name).done(function(accountName) {
191+ record.AccountName(accountName);
192+ });
193+ }
194+ }
195+
196+ function validateAccount(observableRecord) {
197+ showAccountNameValidityFeedback(AccountState.Resolving);
198+
199+ if (observableRecord.UseDBAuthentication == null || observableRecord.UseDBAuthentication()) {
200+ hideAccountNameValidityFeedback();
201+ } else {
202+ if (viewModel.dataHubIsConnected()) {
203+ const accountName = observableRecord.Name();
204+
205+ if (isEmpty(accountName)) {
206+ showAccountNameValidityFeedback(AccountState.Invalid);
207+ } else {
208+ securityHub.userNameToSID(accountName).done(function(sid) {
209+ if (accountName !== sid && accountName.countOccurrences("\\") < 2) {
210+ securityHub.isUserSID(sid).done(function(isUser) {
211+ showAccountNameValidityFeedback(isUser ? AccountState.Valid : AccountState.Unknown);
212+ });
213+ } else {
214+ showAccountNameValidityFeedback(AccountState.Invalid);
215+ }
216+ });
217+ }
218+ } else {
219+ hideAccountNameValidityFeedback();
220+ }
221+ }
222+
223+ return undefined;
224+ }
225+
226+ var messages = [];
227+ var nextMessageName = undefined;
228+ var showingMessage = false;
229+ messages.push("resolvingAccount");
230+ messages.push("accountValid");
231+ messages.push("accountInvalid");
232+ messages.push("accountUnknown");
233+
234+ function showFeedbackMessage(messageName) {
235+ if (showingMessage) {
236+ nextMessageName = messageName;
237+ } else {
238+ showingMessage = true;
239+
240+ hideAccountNameValidityFeedback(messageName);
241+
242+ if (messageName == null)
243+ showingMessage = false;
244+ else
245+ $("#" + messageName).fadeIn(function () {
246+ showingMessage = false;
247+
248+ if (nextMessageName != undefined) {
249+ messageName = nextMessageName;
250+ nextMessageName = undefined;
251+ showFeedbackMessage(messageName);
252+ }
253+ });
254+ }
255+ }
256+
257+ function hideAccountNameValidityFeedback(messageName) {
258+ for (let i = 0; i < messages.length; i++) {
259+ if (messages[i] !== messageName)
260+ $("#" + messages[i]).hide();
261+ }
262+ }
263+
264+ function showAccountNameValidityFeedback(accountState) {
265+ switch (accountState) {
266+ case AccountState.Valid:
267+ showFeedbackMessage("accountValid");
268+ break;
269+ case AccountState.Invalid:
270+ showFeedbackMessage("accountInvalid");
271+ break;
272+ case AccountState.Unknown:
273+ showFeedbackMessage("accountUnknown");
274+ break;
275+ case AccountState.Resolving:
276+ showFeedbackMessage("resolvingAccount");
277+ break;
278+ }
279+ }
280+
281+ $(viewModel).on("pageRecordsQueried", function(event, records) {
282+ // Add a virtual observable field called "AccountName" that will resolve SID values to account names
283+ for (var i = 0; i < records.length; i++) {
284+ records[i].AccountName = ko.observable("...");
285+ lookupAccountName(records[i]);
286+ }
287+ });
288+
289+ $(viewModel).on("newRecord", function(event, newRecord) {
290+ // Make sure new records get virtual field
291+ newRecord.AccountName = ko.observable("");
292+ });
293+
294+ $(viewModel).on("derivingObservableRecord", function(event, observableRecord) {
295+ // Create a virtual UseDBAuthentication field that is negated state of actual UseADAuthentication field
296+ observableRecord.UseDBAuthentication = ko.observable(!observableRecord.UseADAuthentication());
297+
298+ // Load human readable account name into Name field
299+ if (observableRecord.AccountName)
300+ observableRecord.Name(observableRecord.AccountName());
301+
302+ @* Cannot decrypt a password hash, have to clear it and force re-entry *@
303+ if (!observableRecord.UseADAuthentication())
304+ observableRecord.Password(null);
305+ });
306+
307+ $(viewModel).on("derivingJSRecord", function(event, observableRecord, promises) {
308+ // Set state of actual field to be negated state of virtual field
309+ observableRecord.UseADAuthentication(!observableRecord.UseDBAuthentication());
310+
311+ // Remove virtual observable fields before serialization for record updates
312+ delete observableRecord.UseDBAuthentication;
313+ delete observableRecord.AccountName;
314+
315+ // Push promise to convert user name back to SID before updating record
316+ if (viewModel.dataHubIsConnected() && observableRecord.UseADAuthentication()) {
317+ promises.push($.Deferred(function(deferred) {
318+ securityHub.userNameToSID(observableRecord.Name()).done(function(sid) {
319+ observableRecord.Name(sid);
320+ deferred.resolve();
321+ });
322+ }).promise());
323+ }
324+ });
325+
326+ function openRolesEditor(record) {
327+ $("#currentUserID").text(record.ID);
328+ $("#currentUserName").text(record.AccountName());
329+ $("#editRolesDialog input:checkbox").prop("checked", false);
330+
331+ if (viewModel.dataHubIsConnected()) {
332+ securityHub.queryApplicationRoles().done(function(roles) {
333+ var evaluations = roles.length;
334+
335+ if (evaluations > 0)
336+ $("#loadingRoles").show();
337+
338+ for (var i = 0; i < roles.length; i++) {
339+ securityHub.userIsInRole(record.ID, roles[i].ID).done(function(roleID) {
340+ return function(result) {
341+ $("#" + roleID).prop("checked", result);
342+
343+ evaluations--;
344+
345+ if (evaluations <= 0)
346+ $("#loadingRoles").hide();
347+ };
348+ }(roles[i].ID));
349+ }
350+ $("#editRolesDialog").modal("show");
351+ });
352+ }
353+ }
354+
355+ $(function() {
356+ $("#editRolesDialog").modal({ show: false, backdrop: "static" });
357+
358+ $("#saveRolesButton").click(function() {
359+ if (viewModel.dataHubIsConnected()) {
360+ const userID = $("#currentUserID").text();
361+ const roles = $("#editRolesDialog input:checkbox");
362+
363+ for (var i = 0; i < roles.length; i++) {
364+ const element = $(roles[i]);
365+ const roleID = element.attr("id");
366+
367+ if (element.prop("checked")) {
368+ securityHub.addUserToRole(userID, roleID).fail(function(error) {
369+ showErrorMessage(error);
370+ });
371+ } else {
372+ securityHub.removeUserFromRole(userID, roleID).fail(function(error) {
373+ showErrorMessage(error);
374+ });
375+ }
376+ }
377+ }
378+ });
379+ });
380+ </script>
381+}
Source/Libraries/GSF.Web/Templating/NamespaceDoc.cs+35−0
@@ -0,0 +1,35 @@
1+//******************************************************************************************************
2+// NamespaceDoc.cs - Gbtc
3+//
4+// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://www.opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 03/14/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System.Runtime.CompilerServices;
25+
26+namespace GSF.Web.Templating
27+{
28+ /// <summary>
29+ /// Contains classes that provide template based functionality using Razor syntax.
30+ /// </summary>
31+ [CompilerGenerated]
32+ class NamespaceDoc
33+ {
34+ }
35+}
Source/Libraries/GSF.Web/Templating/RazorView.cs+461−0
@@ -0,0 +1,461 @@
1+//******************************************************************************************************
2+// RazorView.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/13/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Generic;
26+using System.IO;
27+using System.Net.Http;
28+using System.Reflection;
29+using System.Threading.Tasks;
30+using System.Web;
31+using System.Web.Hosting;
32+using GSF.Collections;
33+using GSF.Configuration;
34+using GSF.IO;
35+using GSF.Web.Model;
36+using RazorEngine;
37+using RazorEngine.Configuration;
38+using RazorEngine.Templating;
39+
40+// ReSharper disable StaticMemberInGenericType
41+namespace GSF.Web.Templating
42+{
43+ /// <summary>
44+ /// Defines resolutions modes for Razor view template files.
45+ /// </summary>
46+ public enum RazorViewResolutionMode
47+ {
48+ /// <summary>
49+ /// Resolves template files from a path.
50+ /// </summary>
51+ ResolvePath,
52+ /// <summary>
53+ /// Resolves template files from a path and watches for changes.
54+ /// </summary>
55+ /// <remarks>
56+ /// This should only be used in debug mode.
57+ /// </remarks>
58+ WatchingResolvePath,
59+ /// <summary>
60+ /// Resolves template files from an embedded resource.
61+ /// </summary>
62+ EmbeddedResource
63+ }
64+
65+ /// <summary>
66+ /// Defines a language constraint for a <see cref="RazorView{TLanguage}"/>.
67+ /// </summary>
68+ public abstract class LanguageContraint
69+ {
70+ /// <summary>
71+ /// Gets target language for the <see cref="LanguageContraint"/> implementation.
72+ /// </summary>
73+ public abstract Language TargetLanguage
74+ {
75+ get;
76+ }
77+
78+ /// <summary>
79+ /// Gets resolution mode of <see cref="RazorView{TLanguage}"/>.
80+ /// </summary>
81+ public virtual RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.ResolvePath;
82+ }
83+
84+ /// <summary>
85+ /// Defines a C# based language constraint.
86+ /// </summary>
87+ public class CSharp : LanguageContraint
88+ {
89+ /// <summary>
90+ /// Gets target language for the <see cref="LanguageContraint"/> implementation - C#.
91+ /// </summary>
92+ public override Language TargetLanguage => Language.CSharp;
93+ }
94+
95+ /// <summary>
96+ /// Defines a C# based language constraint in debug mode.
97+ /// </summary>
98+ public class CSharpDebug : CSharp
99+ {
100+ /// <summary>
101+ /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - watching resolve path for debugging operations.
102+ /// </summary>
103+ public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.WatchingResolvePath;
104+ }
105+
106+ /// <summary>
107+ /// Defines a C# based language constraint for embedded resources.
108+ /// </summary>
109+ public class CSharpEmbeddedResource : CSharp
110+ {
111+ /// <summary>
112+ /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - embedded resources.
113+ /// </summary>
114+ public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.EmbeddedResource;
115+ }
116+
117+ /// <summary>
118+ /// Defines a Visual Basic based language constraint.
119+ /// </summary>
120+ public class VisualBasic : LanguageContraint
121+ {
122+ /// <summary>
123+ /// Gets target language for the <see cref="LanguageContraint"/> implementation - Visual Basic.
124+ /// </summary>
125+ public override Language TargetLanguage => Language.VisualBasic;
126+ }
127+
128+ /// <summary>
129+ /// Defines a Visual Basic based language constraint in debug mode.
130+ /// </summary>
131+ public class VisualBasicDebug : VisualBasic
132+ {
133+ /// <summary>
134+ /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - watching resolve path for debugging operations.
135+ /// </summary>
136+ public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.WatchingResolvePath;
137+ }
138+
139+ /// <summary>
140+ /// Defines a Visual Basic based language constraint for embedded resources.
141+ /// </summary>
142+ public class VisualBasicEmbeddedResource : CSharp
143+ {
144+ /// <summary>
145+ /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - embedded resources.
146+ /// </summary>
147+ public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.EmbeddedResource;
148+ }
149+
150+ /// <summary>
151+ /// Defines a view class for data context based Razor template implementations.
152+ /// </summary>
153+ /// <typeparam name="TLanguage"><see cref="LanguageContraint"/> type to use for Razor views.</typeparam>
154+ public class RazorView<TLanguage> where TLanguage : LanguageContraint, new()
155+ {
156+ #region [ Members ]
157+
158+ // Fields
159+ private readonly DynamicViewBag m_viewBag = new DynamicViewBag();
160+ private Dictionary<string, string> m_parameters;
161+
162+ #endregion
163+
164+ #region [ Constructors ]
165+
166+ /// <summary>
167+ /// Creates a new <see cref="RazorView{TLanguage}"/>.
168+ /// </summary>
169+ public RazorView()
170+ {
171+ }
172+
173+ /// <summary>
174+ /// Creates a new <see cref="RazorView{TLanguage}"/> with the specified exception handler.
175+ /// </summary>
176+ /// <param name="templateName">Name of template file, typically a .cshtml or .vbhtml file.</param>
177+ /// <param name="exceptionHandler">Delegate to handle exceptions, if any.</param>
178+ public RazorView(string templateName, Action<Exception> exceptionHandler = null)
179+ {
180+ TemplateName = templateName;
181+ ExceptionHandler = exceptionHandler;
182+ }
183+
184+ /// <summary>
185+ /// Creates a new <see cref="RazorView{TLanguage}"/> with the specified parameters.
186+ /// </summary>
187+ /// <param name="templateName">Name of template file, typically a .cshtml or .vbhtml file.</param>
188+ /// <param name="model">Reference to model to use when rendering template.</param>
189+ /// <param name="modelType">Type of <paramref name="model"/>.</param>
190+ /// <param name="exceptionHandler">Delegate to handle exceptions, if any.</param>
191+ public RazorView(string templateName, object model, Type modelType, Action<Exception> exceptionHandler = null) : this(templateName, exceptionHandler)
192+ {
193+ Model = model;
194+ ModelType = modelType;
195+ }
196+
197+ #endregion
198+
199+ #region [ Properties ]
200+
201+ /// <summary>
202+ /// Gets or sets name of template file.
203+ /// </summary>
204+ public string TemplateName
205+ {
206+ get; set;
207+ }
208+
209+ /// <summary>
210+ /// Gets or sets reference to model to use when rendering template.
211+ /// </summary>
212+ public object Model
213+ {
214+ get; set;
215+ }
216+
217+ /// <summary>
218+ /// Gets or sets type of <see cref="Model"/>.
219+ /// </summary>
220+ public Type ModelType
221+ {
222+ get; set;
223+ }
224+
225+ /// <summary>
226+ /// Gets or sets delegate used to handle exceptions.
227+ /// </summary>
228+ public Action<Exception> ExceptionHandler
229+ {
230+ get; set;
231+ }
232+
233+ /// <summary>
234+ /// Gets reference to view bag used when rendering template.
235+ /// </summary>
236+ public dynamic ViewBag => m_viewBag;
237+
238+ /// <summary>
239+ /// Gets query string parameter specified by <paramref name="key"/>.
240+ /// </summary>
241+ /// <param name="key">Name of query string parameter to retrieve.</param>
242+ /// <returns>Query string parameter specified by <paramref name="key"/>.</returns>
243+ public string this[string key] => Parameters[key];
244+
245+ /// <summary>
246+ /// Gets a dictionary of query string parameters passed to rendered view.
247+ /// </summary>
248+ public Dictionary<string, string> Parameters
249+ {
250+ get
251+ {
252+ if ((object)m_parameters == null)
253+ {
254+ HttpRequestMessage request = ViewBag.Request;
255+ m_parameters = HttpUtility.ParseQueryString(request.RequestUri.Query).ToDictionary();
256+ }
257+
258+ return m_parameters;
259+ }
260+ }
261+
262+ #endregion
263+
264+ #region [ Methods ]
265+
266+ /// <summary>
267+ /// Compiles and executes view template.
268+ /// </summary>
269+ /// <returns>Rendered result.</returns>
270+ public string Execute()
271+ {
272+ using (DataContext<TLanguage> dataContext = new DataContext<TLanguage>(exceptionHandler: ExceptionHandler))
273+ {
274+ m_viewBag.AddValue("DataContext", dataContext);
275+ return s_engineService.RunCompile(TemplateName, ModelType, Model, m_viewBag);
276+ }
277+ }
278+
279+ /// <summary>
280+ /// Compiles and executes view template for specified request message and post data.
281+ /// </summary>
282+ /// <returns>Rendered result.</returns>
283+ public string Execute(HttpRequestMessage requestMessage, dynamic postData)
284+ {
285+ using (DataContext<TLanguage> dataContext = new DataContext<TLanguage>(exceptionHandler: ExceptionHandler))
286+ {
287+ m_viewBag.AddValue("DataContext", dataContext);
288+ m_viewBag.AddValue("Request", requestMessage);
289+
290+ if ((object)postData == null)
291+ {
292+ m_viewBag.AddValue("IsPost", false);
293+ }
294+ else
295+ {
296+ m_viewBag.AddValue("IsPost", true);
297+ m_viewBag.AddValue("PostData", postData);
298+ }
299+
300+ return s_engineService.RunCompile(TemplateName, ModelType, Model, m_viewBag);
301+ }
302+ }
303+
304+ /// <summary>
305+ /// Asynchronously compiles and executes view template for specified request message and post data.
306+ /// </summary>
307+ /// <returns>Task that will provide rendered result.</returns>
308+ public Task ExecuteAsync(HttpRequestMessage requestMessage, dynamic postData)
309+ {
310+ return Task.Run(() => Execute(requestMessage, postData));
311+ }
312+
313+ #endregion
314+
315+ #region [ Static ]
316+
317+ // Static Fields
318+ private static readonly IRazorEngineService s_engineService;
319+
320+ /// <summary>
321+ /// Defines default settings path for template files.
322+ /// </summary>
323+ public static readonly string TemplatePath;
324+
325+ // Static Constructor
326+ static RazorView()
327+ {
328+ TLanguage languageType = new TLanguage();
329+
330+ // Get configured template path
331+ CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
332+
333+ switch (languageType.ResolutionMode)
334+ {
335+ case RazorViewResolutionMode.ResolvePath:
336+ case RazorViewResolutionMode.WatchingResolvePath:
337+ if (Common.GetApplicationType() == ApplicationType.Web)
338+ {
339+ systemSettings.Add("TemplatePath", "~/Views/Shared/Templates/", "Path for data context based razor field templates.");
340+ TemplatePath = HostingEnvironment.MapPath(systemSettings["TemplatePath"].Value).EnsureEnd('/');
341+ }
342+ else
343+ {
344+ systemSettings.Add("TemplatePath", "wwwroot/Templates/", "Path for data context based razor field templates.");
345+ TemplatePath = FilePath.GetAbsolutePath(systemSettings["TemplatePath"].Value);
346+ }
347+
348+ if (languageType.ResolutionMode == RazorViewResolutionMode.ResolvePath)
349+ {
350+ s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
351+ {
352+ Language = languageType.TargetLanguage,
353+ TemplateManager = new ResolvePathTemplateManager(new[] { TemplatePath }),
354+ Debug = false
355+ });
356+ }
357+ else
358+ {
359+ // The watching resolve path template manager should not be used in production since
360+ // assemblies cannot be unloaded from an AppDomain. Every time a change to a .cshtml
361+ // file is picked up by the watcher it is compiled and loaded into the AppDomain and
362+ // the old one cannot be removed (.NET restriction), the net result is a memory leak
363+ InvalidatingCachingProvider cachingProvider = new InvalidatingCachingProvider();
364+
365+ s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
366+ {
367+ Language = languageType.TargetLanguage,
368+ CachingProvider = cachingProvider,
369+ TemplateManager = new WatchingResolvePathTemplateManager(new[] { TemplatePath }, cachingProvider),
370+ Debug = true
371+ });
372+ }
373+ break;
374+ case RazorViewResolutionMode.EmbeddedResource:
375+ systemSettings.Add("EmbeddedTemplatePath", "GSF.Web.Templating.Views.", "Embedded name space path for data context based razor field templates.");
376+ TemplatePath = systemSettings["EmbeddedTemplatePath"].Value.EnsureEnd('.');
377+
378+ s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
379+ {
380+ Language = languageType.TargetLanguage,
381+ TemplateManager = new DelegateTemplateManager(name =>
382+ {
383+ string resourceName = name;
384+
385+ if (!resourceName.Contains("."))
386+ resourceName = TemplatePath + name;
387+
388+ Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
389+
390+ if ((object)stream == null)
391+ return "";
392+
393+ using (StreamReader reader = new StreamReader(stream))
394+ {
395+ return reader.ReadToEnd();
396+ }
397+ }),
398+ Debug = false
399+ });
400+
401+
402+
403+ break;
404+ default:
405+ throw new ArgumentOutOfRangeException("");
406+ }
407+ }
408+
409+ /// <summary>
410+ /// Kicks off a task to pre-compile Razor templates.
411+ /// </summary>
412+ /// <param name="exceptionHandler">Exception handler used to report issues, if any.</param>
413+ public static Task PreCompile(Action<Exception> exceptionHandler = null)
414+ {
415+ return Task.Run(() =>
416+ {
417+ TLanguage languageType = new TLanguage();
418+
419+ if (languageType.ResolutionMode == RazorViewResolutionMode.EmbeddedResource)
420+ {
421+ foreach (string fileName in Assembly.GetExecutingAssembly().GetManifestResourceNames())
422+ {
423+ if (fileName.StartsWith(TemplatePath))
424+ {
425+ try
426+ {
427+ s_engineService.Compile(fileName.Substring(TemplatePath.Length));
428+ }
429+ catch (Exception ex)
430+ {
431+ if ((object)exceptionHandler != null)
432+ exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex));
433+ }
434+ }
435+
436+ }
437+ }
438+ else
439+ {
440+ string webRootFolder = FilePath.AddPathSuffix(TemplatePath);
441+ string[] razorFiles = FilePath.GetFileList($"{webRootFolder}*.{(languageType.TargetLanguage == Language.CSharp ? "cs" : "vb")}html");
442+
443+ foreach (string fileName in razorFiles)
444+ {
445+ try
446+ {
447+ s_engineService.Compile(FilePath.GetFileName(fileName));
448+ }
449+ catch (Exception ex)
450+ {
451+ if ((object)exceptionHandler != null)
452+ exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex));
453+ }
454+ }
455+ }
456+ });
457+ }
458+
459+ #endregion
460+ }
461+}
Source/Libraries/GSF.Web/Templating/Views/AddCheckBoxField.cshtml+70−0
@@ -0,0 +1,70 @@
1+@*******************************************************************************************************
2+// AddCheckBoxField.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using RazorEngine.Templating
24+@inherits TemplateBase
25+@{
26+ string fieldName = ViewBag.FieldName;
27+ string fieldLabel = ViewBag.FieldLabel;
28+ string fieldID = ViewBag.FieldID;
29+ string groupDataBinding = ViewBag.GroupDataBinding;
30+ string labelDataBinding = ViewBag.LabelDataBinding;
31+ string customDataBinding = ViewBag.CustomDataBinding;
32+ string dependencyFieldName = ViewBag.DependencyFieldName;
33+ string toolTip = ViewBag.ToolTip;
34+
35+ if (!string.IsNullOrEmpty(dependencyFieldName))
36+ {
37+ if (string.IsNullOrEmpty(customDataBinding))
38+ {
39+ customDataBinding = string.Format("enable: {0}", dependencyFieldName);
40+ }
41+
42+ if (string.IsNullOrEmpty(labelDataBinding))
43+ {
44+ labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName);
45+ }
46+ }
47+
48+ if (!string.IsNullOrEmpty(groupDataBinding))
49+ {
50+ groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding);
51+ }
52+
53+ if (!string.IsNullOrEmpty(labelDataBinding))
54+ {
55+ labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding);
56+ }
57+
58+ if (!string.IsNullOrEmpty(customDataBinding))
59+ {
60+ customDataBinding = string.Format(", {0}", customDataBinding);
61+ }
62+
63+ if (!string.IsNullOrEmpty(toolTip))
64+ {
65+ toolTip = string.Format(" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"{0}\"", toolTip);
66+ }
67+}
68+<div class="checkbox"@Raw(groupDataBinding)@Raw(toolTip)>
69+ <label for="@Raw(fieldID)"@Raw(labelDataBinding)><input type="checkbox" id="@Raw(fieldID)" data-bind="checked: @Raw(fieldName), attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)"> @Raw(fieldLabel)</label>
70+</div>
Source/Libraries/GSF.Web/Templating/Views/AddDateField.cshtml+109−0
@@ -0,0 +1,109 @@
1+@*******************************************************************************************************
2+// AddDateField.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using RazorEngine.Templating
24+@inherits TemplateBase
25+@{
26+ string fieldName = ViewBag.FieldName;
27+ bool required = ViewBag.Required;
28+ int maxLength = ViewBag.MaxLength;
29+ string inputType = ViewBag.InputType;
30+ string fieldLabel = ViewBag.FieldLabel;
31+ string fieldID = ViewBag.FieldID;
32+ string groupDataBinding = ViewBag.GroupDataBinding;
33+ string labelDataBinding = ViewBag.LabelDataBinding;
34+ string requiredDataBinding = ViewBag.RequiredDataBinding;
35+ string customDataBinding = ViewBag.CustomDataBinding;
36+ string dependencyFieldName = ViewBag.DependencyFieldName;
37+ string toolTip = ViewBag.ToolTip;
38+ string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : "";
39+
40+ if (!string.IsNullOrEmpty(dependencyFieldName))
41+ {
42+ if (string.IsNullOrEmpty(customDataBinding))
43+ {
44+ customDataBinding = string.Format("enable: {0}", dependencyFieldName);
45+ }
46+
47+ if (string.IsNullOrEmpty(labelDataBinding))
48+ {
49+ labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName);
50+ }
51+
52+ if (required && string.IsNullOrEmpty(requiredDataBinding))
53+ {
54+ requiredDataBinding = string.Format("{0}()", dependencyFieldName);
55+ }
56+ }
57+
58+ if (!string.IsNullOrEmpty(groupDataBinding))
59+ {
60+ groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding);
61+ }
62+
63+ if (!string.IsNullOrEmpty(labelDataBinding))
64+ {
65+ labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding);
66+ }
67+
68+ if (!string.IsNullOrEmpty(requiredDataBinding))
69+ {
70+ requiredDataBinding = string.Format(" && {0}", requiredDataBinding);
71+ }
72+
73+ if (!string.IsNullOrEmpty(customDataBinding))
74+ {
75+ customDataBinding = string.Format(", {0}", customDataBinding);
76+ }
77+
78+ if (!string.IsNullOrEmpty(toolTip))
79+ {
80+ toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip);
81+ }
82+}
83+@if (required)
84+{
85+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding))
86+ <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)>
87+ @if (fieldLabel.Length > 0) {
88+ <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label>
89+ }
90+ <div class="input-group date" data-provide="datepicker">
91+ <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")>
92+ <span class="input-group-addon" data-bind="style: {'cursor': ($root.recordMode()===RecordMode.View ? 'not-allowed' : 'pointer')}"><i class="glyphicon glyphicon-calendar"></i></span>
93+ <span class="glyphicon glyphicon-remove form-control-feedback" style="right: 36px" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span>
94+ </div>
95+ </div>
96+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n")
97+}
98+else
99+{
100+ <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)>
101+ @if (fieldLabel.Length > 0) {
102+ <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label>
103+ }
104+ <div class="input-group date" data-provide="datepicker">
105+ <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)>
106+ <span class="input-group-addon" data-bind="style: {'cursor': ($root.recordMode()===RecordMode.View ? 'not-allowed' : 'pointer')}"><i class="glyphicon glyphicon-calendar"></i></span>
107+ </div>
108+ </div>
109+}
Source/Libraries/GSF.Web/Templating/Views/AddInputField.cshtml+103−0
@@ -0,0 +1,103 @@
1+@*******************************************************************************************************
2+// AddInputField.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using RazorEngine.Templating
24+@inherits TemplateBase
25+@{
26+ string fieldName = ViewBag.FieldName;
27+ bool required = ViewBag.Required;
28+ int maxLength = ViewBag.MaxLength;
29+ string inputType = ViewBag.InputType;
30+ string fieldLabel = ViewBag.FieldLabel;
31+ string fieldID = ViewBag.FieldID;
32+ string groupDataBinding = ViewBag.GroupDataBinding;
33+ string labelDataBinding = ViewBag.LabelDataBinding;
34+ string requiredDataBinding = ViewBag.RequiredDataBinding;
35+ string customDataBinding = ViewBag.CustomDataBinding;
36+ string dependencyFieldName = ViewBag.DependencyFieldName;
37+ string toolTip = ViewBag.ToolTip;
38+ string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : "";
39+
40+ if (!string.IsNullOrEmpty(dependencyFieldName))
41+ {
42+ if (string.IsNullOrEmpty(customDataBinding))
43+ {
44+ customDataBinding = string.Format("enable: {0}", dependencyFieldName);
45+ }
46+
47+ if (string.IsNullOrEmpty(labelDataBinding))
48+ {
49+ labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName);
50+ }
51+
52+ if (required && string.IsNullOrEmpty(requiredDataBinding))
53+ {
54+ requiredDataBinding = string.Format("{0}()", dependencyFieldName);
55+ }
56+ }
57+
58+ if (!string.IsNullOrEmpty(groupDataBinding))
59+ {
60+ groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding);
61+ }
62+
63+ if (!string.IsNullOrEmpty(labelDataBinding))
64+ {
65+ labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding);
66+ }
67+
68+ if (!string.IsNullOrEmpty(requiredDataBinding))
69+ {
70+ requiredDataBinding = string.Format(" && {0}", requiredDataBinding);
71+ }
72+
73+ if (!string.IsNullOrEmpty(customDataBinding))
74+ {
75+ customDataBinding = string.Format(", {0}", customDataBinding);
76+ }
77+
78+ if (!string.IsNullOrEmpty(toolTip))
79+ {
80+ toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip);
81+ }
82+}
83+@if (required)
84+{
85+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding))
86+ <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)>
87+ @if (fieldLabel.Length > 0) {
88+ <label for="@Raw(fieldID)"@Raw(labelDataBinding)>@Raw(fieldLabel):</label>
89+ }
90+ <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")>
91+ <span class="glyphicon glyphicon-remove form-control-feedback" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span>
92+ </div>
93+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n")
94+}
95+else
96+{
97+ <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)>
98+ @if (fieldLabel.Length > 0) {
99+ <label for="@Raw(fieldID)"@Raw(labelDataBinding)>@Raw(fieldLabel):</label>
100+ }
101+ <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)>
102+ </div>
103+}
Source/Libraries/GSF.Web/Templating/Views/AddSelectField.cshtml+86−0
@@ -0,0 +1,86 @@
1+@*******************************************************************************************************
2+// AddSelectField.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using System.Collections.Generic
24+@using RazorEngine.Templating
25+@inherits TemplateBase
26+@{
27+ string fieldName = ViewBag.FieldName;
28+ bool required = ViewBag.Required;
29+ string fieldLabel = ViewBag.FieldLabel;
30+ string fieldID = ViewBag.FieldID;
31+ string groupDataBinding = ViewBag.GroupDataBinding;
32+ string labelDataBinding = ViewBag.LabelDataBinding;
33+ string customDataBinding = ViewBag.CustomDataBinding;
34+ string dependencyFieldName = ViewBag.DependencyFieldName;
35+ string toolTip = ViewBag.ToolTip;
36+ string optionDataBinding = ViewBag.OptionDataBinding;
37+ Dictionary<string, string> options = ViewBag.Options;
38+
39+ if (!string.IsNullOrEmpty(dependencyFieldName))
40+ {
41+ if (string.IsNullOrEmpty(customDataBinding))
42+ {
43+ customDataBinding = string.Format("enable: {0}", dependencyFieldName);
44+ }
45+
46+ if (string.IsNullOrEmpty(labelDataBinding))
47+ {
48+ labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName);
49+ }
50+ }
51+
52+ if (!string.IsNullOrEmpty(groupDataBinding))
53+ {
54+ groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding);
55+ }
56+
57+ if (!string.IsNullOrEmpty(labelDataBinding))
58+ {
59+ labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding);
60+ }
61+
62+ if (!string.IsNullOrEmpty(customDataBinding))
63+ {
64+ customDataBinding = string.Format(", {0}", customDataBinding);
65+ }
66+
67+ if (!string.IsNullOrEmpty(toolTip))
68+ {
69+ toolTip = string.Format(" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"{0}\"", toolTip);
70+ }
71+
72+ if (!string.IsNullOrEmpty(optionDataBinding))
73+ {
74+ optionDataBinding = string.Format(" data-bind=\"{0}\"", optionDataBinding);
75+ }
76+}
77+<div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)>
78+ @if (fieldLabel.Length > 0) {
79+ <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label>
80+ }
81+ <select class="form-control" id="@Raw(fieldID)" data-bind="value: @Raw(fieldName), valueAllowUnset: @((!required).ToString().ToLower()), attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)">
82+ @foreach (KeyValuePair<string, string> item in options) {
83+ <option value="@item.Key"@Raw(optionDataBinding)>@item.Value</option>
84+ }
85+ </select>
86+</div>
Source/Libraries/GSF.Web/Templating/Views/AddTextAreaField.cshtml+103−0
@@ -0,0 +1,103 @@
1+@*******************************************************************************************************
2+// AddTextAreaField.cshtml - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/01/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//*****************************************************************************************************@
23+@using RazorEngine.Templating
24+@inherits TemplateBase
25+@{
26+ string fieldName = ViewBag.FieldName;
27+ bool required = ViewBag.Required;
28+ int maxLength = ViewBag.MaxLength;
29+ int rows = ViewBag.Rows;
30+ string fieldLabel = ViewBag.FieldLabel;
31+ string fieldID = ViewBag.FieldID;
32+ string groupDataBinding = ViewBag.GroupDataBinding;
33+ string labelDataBinding = ViewBag.LabelDataBinding;
34+ string requiredDataBinding = ViewBag.RequiredDataBinding;
35+ string customDataBinding = ViewBag.CustomDataBinding;
36+ string dependencyFieldName = ViewBag.DependencyFieldName;
37+ string toolTip = ViewBag.ToolTip;
38+ string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : "";
39+
40+ if (!string.IsNullOrEmpty(dependencyFieldName))
41+ {
42+ if (string.IsNullOrEmpty(customDataBinding))
43+ {
44+ customDataBinding = string.Format("enable: {0}", dependencyFieldName);
45+ }
46+
47+ if (string.IsNullOrEmpty(labelDataBinding))
48+ {
49+ labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName);
50+ }
51+
52+ if (required && string.IsNullOrEmpty(requiredDataBinding))
53+ {
54+ requiredDataBinding = string.Format("{0}()", dependencyFieldName);
55+ }
56+ }
57+
58+ if (!string.IsNullOrEmpty(groupDataBinding))
59+ {
60+ groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding);
61+ }
62+
63+ if (!string.IsNullOrEmpty(labelDataBinding))
64+ {
65+ labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding);
66+ }
67+
68+ if (!string.IsNullOrEmpty(requiredDataBinding))
69+ {
70+ requiredDataBinding = string.Format(" && {0}", requiredDataBinding);
71+ }
72+
73+ if (!string.IsNullOrEmpty(customDataBinding))
74+ {
75+ customDataBinding = string.Format(", {0}", customDataBinding);
76+ }
77+
78+ if (!string.IsNullOrEmpty(toolTip))
79+ {
80+ toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip);
81+ }
82+}
83+@if (required)
84+{
85+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding))
86+ <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)>
87+ @if (fieldLabel.Length > 0) {
88+ <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label>
89+ }
90+ <textarea rows="@rows" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")></textarea>
91+ <span class="glyphicon glyphicon-remove form-control-feedback" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span>
92+ </div>
93+ @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n")
94+}
95+else
96+{
97+ <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)>
98+ @if (fieldLabel.Length > 0) {
99+ <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label>
100+ }
101+ <textarea rows="@rows" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)></textarea>
102+ </div>
103+}
Source/Libraries/GSF.Web/WebExtensions.cs+102−0
@@ -0,0 +1,102 @@
1+//******************************************************************************************************
2+// WebExtensions.cs - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
9+// file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 02/17/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+using System;
25+using System.Collections.Generic;
26+using System.Linq;
27+using System.Net.Http;
28+using System.Web;
29+using GSF.Collections;
30+
31+namespace GSF.Web
32+{
33+ /// <summary>
34+ /// Defines extension methods useful for web applications.
35+ /// </summary>
36+ public static class WebExtensions
37+ {
38+ /// <summary>
39+ /// Performs JavaScript encoding on given string.
40+ /// </summary>
41+ /// <param name="text">The string to be encoded.</param>
42+ public static string JavaScriptEncode(this string text)
43+ {
44+ return HttpUtility.JavaScriptStringEncode(text.ToNonNullString());
45+ }
46+
47+ /// <summary>
48+ /// Performs HTML encoding on the given string.
49+ /// </summary>
50+ /// <param name="text">The string to be encoded.</param>
51+ public static string HtmlEncode(this string text)
52+ {
53+ return HttpUtility.HtmlEncode(text.ToNonNullString());
54+ }
55+
56+ /// <summary>
57+ /// Performs URL encoding on the given string.
58+ /// </summary>
59+ /// <param name="text">The string to be encoded.</param>
60+ public static string UrlEncode(this string text)
61+ {
62+ return HttpUtility.UrlEncode(text.ToNonNullString());
63+ }
64+
65+ /// <summary>
66+ /// Gets query parameters for current request message
67+ /// </summary>
68+ /// <param name="request"></param>
69+ /// <returns></returns>
70+ public static Dictionary<string, string> QueryParameters(this HttpRequestMessage request)
71+ {
72+ return request.GetQueryNameValuePairs().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
73+ }
74+
75+ /// <summary>
76+ /// Corrects script alignment with a desired number of forward spaces.
77+ /// </summary>
78+ /// <param name="script">Script text.</param>
79+ /// <param name="spaces">Desired forward spaces.</param>
80+ /// <returns>Script with corrected forward alignment.</returns>
81+ public static string FixForwardSpacing(this string script, int spaces = 4)
82+ {
83+ Tuple<string, int>[] linesAndLengths = script
84+ .Split(new[] { Environment.NewLine }, StringSplitOptions.None)
85+ .Select(line => new Tuple<string, int>(line, line.Length - line.TrimStart(' ').Length))
86+ .ToArray();
87+
88+ int minLength = linesAndLengths
89+ .Select(lineAndLength => (int?)lineAndLength.Item2)
90+ .Where(length => length.GetValueOrDefault() > 0)
91+ .Min() ?? 0;
92+
93+ string forwardSpacing = new string(' ', spaces);
94+
95+ return linesAndLengths
96+ .Select(lineAndLength => lineAndLength.Item2 > 0 ?
97+ $"{forwardSpacing}{(lineAndLength.Item2 > minLength ? lineAndLength.Item1.Substring(minLength) : lineAndLength.Item1)}" :
98+ lineAndLength.Item1.ToNonNullNorEmptyString())
99+ .ToDelimitedString(Environment.NewLine);
100+ }
101+ }
102+}
Source/Libraries/GSF.Web/packages.config+20−0
@@ -0,0 +1,20 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<packages>
3+ <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net46" />
4+ <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net46" />
5+ <package id="Microsoft.AspNet.SignalR" version="2.2.0" targetFramework="net46" />
6+ <package id="Microsoft.AspNet.SignalR.Core" version="2.2.0" targetFramework="net46" />
7+ <package id="Microsoft.AspNet.SignalR.JS" version="2.2.0" targetFramework="net46" />
8+ <package id="Microsoft.AspNet.SignalR.SystemWeb" version="2.2.0" targetFramework="net46" />
9+ <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net46" />
10+ <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net46" />
11+ <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net46" />
12+ <package id="Microsoft.Owin" version="2.1.0" targetFramework="net46" />
13+ <package id="Microsoft.Owin.Host.SystemWeb" version="2.1.0" targetFramework="net46" />
14+ <package id="Microsoft.Owin.Security" version="2.1.0" targetFramework="net46" />
15+ <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net46" />
16+ <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net46" />
17+ <package id="Owin" version="1.0" targetFramework="net46" />
18+ <package id="RazorEngine" version="3.7.7" targetFramework="net46" />
19+ <package id="System.Net.Http" version="2.0.20126.16343" targetFramework="net46" />
20+</packages>

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t