Ian Wright 8 年 前
コミット
364989d115
100 ファイル変更2324 行追加797 行削除
  1. 1 1
      LICENSE
  2. 4 9
      README.md
  3. 8 3
      checkstyle.xml
  4. 126 0
      import-control.xml
  5. 142 43
      pom.xml
  6. 22 0
      src/assembly/pwm.xml
  7. 0 1
      src/main/angular/images/icons/m_chevron-down.svg
  8. 0 1
      src/main/angular/images/icons/m_chevron-up.svg
  9. 22 0
      src/main/angular/images/icons/m_circle-horz-menu.svg
  10. 0 1
      src/main/angular/images/icons/m_close.svg
  11. 23 0
      src/main/angular/images/icons/m_close_thick.svg
  12. 23 0
      src/main/angular/images/icons/m_configure_thin.svg
  13. 2 12
      src/main/angular/images/icons/m_down_thick.svg
  14. 0 1
      src/main/angular/images/icons/m_edit.svg
  15. 0 1
      src/main/angular/images/icons/m_magnify.svg
  16. 0 25
      src/main/angular/images/icons/m_next-right.svg
  17. 22 0
      src/main/angular/images/icons/m_orgchart.svg
  18. 0 1
      src/main/angular/images/icons/m_question_mark.svg
  19. 23 0
      src/main/angular/images/icons/m_search_thick.svg
  20. 0 1
      src/main/angular/images/icons/m_settings.svg
  21. 23 0
      src/main/angular/images/icons/m_up_thick.svg
  22. 22 0
      src/main/angular/images/icons/m_view-list.svg
  23. 22 0
      src/main/angular/images/icons/m_view-tile.svg
  24. 22 1
      src/main/angular/index.html
  25. 1 1
      src/main/angular/karma.conf.js
  26. 1 1
      src/main/angular/src/component.ts
  27. 5 1
      src/main/angular/src/main.dev.ts
  28. 7 3
      src/main/angular/src/main.ts
  29. 1 1
      src/main/angular/src/models/column.model.ts
  30. 5 5
      src/main/angular/src/models/orgchart-data.model.ts
  31. 14 35
      src/main/angular/src/models/person.model.ts
  32. 4 4
      src/main/angular/src/models/search-result.model.ts
  33. 31 4
      src/main/angular/src/peoplesearch/orgchart-search.component.html
  34. 2 2
      src/main/angular/src/peoplesearch/orgchart-search.component.scss
  35. 84 35
      src/main/angular/src/peoplesearch/orgchart-search.component.ts
  36. 28 9
      src/main/angular/src/peoplesearch/orgchart.component.html
  37. 14 5
      src/main/angular/src/peoplesearch/orgchart.component.scss
  38. 1 1
      src/main/angular/src/peoplesearch/orgchart.component.test.ts
  39. 13 22
      src/main/angular/src/peoplesearch/orgchart.component.ts
  40. 144 50
      src/main/angular/src/peoplesearch/peoplesearch-base.component.ts
  41. 40 9
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.html
  42. 5 3
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss
  43. 65 17
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts
  44. 36 6
      src/main/angular/src/peoplesearch/peoplesearch-table.component.html
  45. 2 2
      src/main/angular/src/peoplesearch/peoplesearch-table.component.scss
  46. 40 10
      src/main/angular/src/peoplesearch/peoplesearch-table.component.ts
  47. 7 3
      src/main/angular/src/peoplesearch/peoplesearch.module.ts
  48. 30 13
      src/main/angular/src/peoplesearch/peoplesearch.scss
  49. 24 2
      src/main/angular/src/peoplesearch/person-card.component.html
  50. 26 6
      src/main/angular/src/peoplesearch/person-card.component.scss
  51. 22 11
      src/main/angular/src/peoplesearch/person-card.component.ts
  52. 77 54
      src/main/angular/src/peoplesearch/person-details-dialog.component.html
  53. 86 53
      src/main/angular/src/peoplesearch/person-details-dialog.component.scss
  54. 25 8
      src/main/angular/src/peoplesearch/person-details-dialog.component.ts
  55. 4 4
      src/main/angular/src/peoplesearch/person.filters.ts
  56. 1 1
      src/main/angular/src/peoplesearch/string.filters.ts
  57. 37 0
      src/main/angular/src/route-error-handler.ts
  58. 46 10
      src/main/angular/src/routes.ts
  59. 14 2
      src/main/angular/src/services/config.service.dev.ts
  60. 47 8
      src/main/angular/src/services/config.service.ts
  61. 67 0
      src/main/angular/src/services/local-storage.service.ts
  62. 147 21
      src/main/angular/src/services/people.data.json
  63. 70 39
      src/main/angular/src/services/people.service.dev.ts
  64. 125 57
      src/main/angular/src/services/people.service.ts
  65. 9 13
      src/main/angular/src/services/promise.service.ts
  66. 42 0
      src/main/angular/src/services/pwm.service.dev.ts
  67. 20 12
      src/main/angular/src/services/pwm.service.ts
  68. 4 11
      src/main/angular/src/services/translations-loader.factory.ts
  69. 5 3
      src/main/angular/src/ux/app-bar.component.scss
  70. 1 1
      src/main/angular/src/ux/app-bar.component.ts
  71. 13 3
      src/main/angular/src/ux/auto-complete.component.scss
  72. 13 5
      src/main/angular/src/ux/auto-complete.component.ts
  73. 10 3
      src/main/angular/src/ux/button.component.scss
  74. 1 1
      src/main/angular/src/ux/button.component.ts
  75. 23 1
      src/main/angular/src/ux/dialog.component.html
  76. 5 17
      src/main/angular/src/ux/dialog.component.scss
  77. 1 1
      src/main/angular/src/ux/dialog.component.ts
  78. 1 1
      src/main/angular/src/ux/dialog.service.ts
  79. 1 1
      src/main/angular/src/ux/element-size.service.ts
  80. 10 3
      src/main/angular/src/ux/icon-button.component.scss
  81. 1 1
      src/main/angular/src/ux/icon-button.component.ts
  82. 2 2
      src/main/angular/src/ux/icon.component.scss
  83. 1 1
      src/main/angular/src/ux/icon.component.ts
  84. 27 5
      src/main/angular/src/ux/search-bar.component.html
  85. 6 2
      src/main/angular/src/ux/search-bar.component.scss
  86. 15 4
      src/main/angular/src/ux/search-bar.component.ts
  87. 1 1
      src/main/angular/src/ux/table-column.directive.ts
  88. 1 1
      src/main/angular/src/ux/table.directive.controller.ts
  89. 40 15
      src/main/angular/src/ux/table.directive.html
  90. 67 40
      src/main/angular/src/ux/table.directive.scss
  91. 1 1
      src/main/angular/src/ux/table.directive.ts
  92. 1 1
      src/main/angular/src/ux/ux.module.ts
  93. 22 0
      src/main/angular/vendor/angular-ui-router.js
  94. 1 1
      src/main/angular/webpack.build.js
  95. 1 1
      src/main/angular/webpack.common.js
  96. 1 1
      src/main/angular/webpack.dev.js
  97. 1 1
      src/main/angular/webpack.test.js
  98. 45 11
      src/main/java/password/pwm/AppProperty.java
  99. 1 1
      src/main/java/password/pwm/Permission.java
  100. 25 12
      src/main/java/password/pwm/PwmAboutProperty.java

+ 1 - 1
LICENSE

@@ -2,7 +2,7 @@ Password Management Servlets (PWM)
 http://www.pwm-project.org
 
 Copyright (c) 2006-2009 Novell, Inc.
-Copyright (c) 2009-2016 The PWM Project
+Copyright (c) 2009-2017 The PWM Project
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by

+ 4 - 9
README.md

@@ -5,15 +5,10 @@ PWM is an open source password self service application for LDAP directories. PW
 Official project page is at [https://github.com/pwm-project/pwm/](https://github.com/pwm-project/pwm/).
 
 # Links
-Current (v1.8)
 * [PWM-General Google Group](https://groups.google.com/group/pwm-general) - please ask for assistance here first.
-* [Current Builds](http://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
-* [PWM Documentation Wiki](https://github.com/pwm-project/pwm/wiki) - Current home for PWM documentation
-* [PWM Reference](http://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
-
-Old (v1.7)
-* [Old Release Downloads](https://drive.google.com/folderview?id=0B3oHdiTrftrGV3ZrMi1LUzVCY1U&usp=sharing#list) - the release versions are quite dated, consider using a current build.
-* [Old PWM Admin Guide](https://docs.google.com/document/d/1BBHPcOUxZytrqncYFInTaY2PXgW5p1EmzwV8zcpspRg/pub) - guide for 1.7.  For current documentation, please help us migrate to the [PWM Wiki](https://github.com/pwm-project/pwm/wiki) 
+* [PWM Documentation Wiki](https://github.com/pwm-project/pwm/wiki) - Home for PWM documentation
+* [Current Builds](https://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
+* [PWM Reference](https://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
 
 Features
 * Web based configuration manager with over 400 configurable settings
@@ -27,7 +22,7 @@ Features
 * Helpdesk password reset and intruder lockout clearing
 * New User Registration / Account Creation
 * Guest User Registration / Updating
-* PeopleSearch? (white pages)
+* PeopleSearch (white pages)
 * Account Activation  / First time password assignment
 * Administration modules including intruder-lockout manager, and online log viewer, daily stats viewer and user information debugging
 * Easy to customize JSP HTML pages

+ 8 - 3
checkstyle.xml

@@ -84,9 +84,6 @@
         <!--
         <module name="FileContentsHolder"/>
 
-        <module name="LeftCurly">
-            <property name="option" value="nl"/>
-        </module>
 
         -->
 
@@ -178,6 +175,9 @@
         <module name="IllegalImport"/>
         <module name="RedundantImport"/>
         <module name="UnusedImports"/>
+        <module name="ImportControl">
+            <property name="file" value="import-control.xml"/>
+        </module>
 
 
         <!-- Checks for Size Violations.                    -->
@@ -295,6 +295,11 @@
         <module name="ParameterAssignment"/>
         <module name="SimplifyBooleanReturn"/>
         <module name="StringLiteralEquality"/>
+        <module name="CovariantEquals"/>
+        <module name="DefaultComesLast"/>
+        <module name="EmptyStatement"/>
+        <module name="EqualsHashCode"/>
+        <module name="EqualsAvoidNull"/>
 
         <module name="MutableException"/>
         <module name="OuterTypeFilename"/>

+ 126 - 0
import-control.xml

@@ -0,0 +1,126 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2016 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<!DOCTYPE import-control PUBLIC
+        "-//Puppy Crawl//DTD Import Control 1.2//EN"
+        "http://checkstyle.sourceforge.net/dtds/import_control_1_2.dtd">
+<import-control pkg="password.pwm">
+    <allow pkg="password.pwm"/>
+
+    <!-- java core -->
+    <allow pkg="java.text"/>
+    <allow pkg="java.math"/>
+    <allow pkg="java.io"/>
+    <allow pkg="java.net"/>
+    <allow pkg="java.nio"/>
+    <allow pkg="java.util"/>
+    <allow pkg="java.time"/>
+    <allow pkg="java.lang"/>
+    <allow pkg="java.security"/>
+
+    <!-- chai -->
+    <allow pkg="com.novell.ldapchai"/>
+
+    <!-- xml  -->
+    <allow pkg="org.jdom2"/>
+    <allow pkg="javax.xml"/>
+    <allow pkg="org.w3c"/>
+    <allow pkg="org.xml"/>
+
+    <!-- graylog2 -->
+    <allow pkg="org.graylog2"/>
+
+    <!-- log4j -->
+    <allow pkg="org.apache.log4j"/>
+
+    <!-- gson -->
+    <allow pkg="com.google.gson"/>
+
+    <!-- to be removed/scoped -->
+    <allow pkg="org.apache.http"/>
+    <allow pkg="org.apache.commons"/>
+    <allow pkg="org.jasig.cas"/>
+    <allow pkg="net.iharder"/>
+    <allow pkg="org.jetbrains.annotations"/>
+    <allow pkg="com.novell.ldap"/>
+    <allow pkg="java.security"/>
+    <allow pkg="javax.swing"/>
+    <allow pkg="java.awt"/>
+    <allow pkg="javax.security"/>
+    <allow pkg="eu.bitwalker.useragentutils"/>
+    <allow pkg="javax.servlet"/>
+    <allow pkg="javax.net"/>
+    <allow pkg="javax.crypto"/>
+    <allow pkg="javax.mail"/>
+    <allow pkg="org.xeustechnologies"/>
+    <allow pkg="net.glxn"/>
+    <allow pkg="org.webjars"/>
+    <allow pkg="lombok"/>
+    <allow pkg="com.github.benmanes.caffeine"/>
+
+
+    <!--servlet -->
+    <subpackage name="http.servlet">
+        <allow pkg="javax.servlet"/>
+    </subpackage>
+
+    <!-- web services -->
+    <subpackage name="ws.server">
+        <allow pkg="javax.ws"/>
+        <allow pkg="javax.servlet"/>
+        <allow pkg="org.glassfish.jersey"/>
+    </subpackage>
+
+    <!-- security -->
+    <subpackage name="util.secure">
+        <allow pkg="org.bouncycastle"/>
+        <allow pkg="javax.net"/>
+    </subpackage>
+
+    <!-- nmas -->
+    <subpackage name="util.operations.cr">
+        <allow pkg="com.novell.ldap"/>
+        <allow pkg="com.novell.security.nmas"/>
+    </subpackage>
+
+    <!-- database -->
+    <subpackage name="util.db">
+        <allow pkg="java.sql"/>
+    </subpackage>
+
+    <subpackage name="util.java">
+        <allow class="net.iharder.Base64"/>
+        <allow pkg="org.apache.commons.codec"/>
+        <allow pkg="org.apache.commons.lang3"/>
+    </subpackage>
+
+    <subpackage name="util.localdb">
+        <allow pkg="jetbrains.exodus"/>
+        <allow pkg="java.sql"/>
+    </subpackage>
+
+    <subpackage name="http.client">
+        <allow pkg="org.apache.http"/>
+    </subpackage>
+
+
+</import-control>

+ 142 - 43
pom.xml

@@ -22,7 +22,7 @@
     </licenses>
 
     <organization>
-        <name>Pwm Project</name>
+        <name>PWM Project</name>
         <url>http://www.pwm-project.org</url>
     </organization>
 
@@ -37,22 +37,53 @@
         <build.revision>0</build.revision>  <!-- default in case not set on command line -->
 		<!-- Properties used for CAS configuration -->
 		<cas.server>https://cas.localdomain.local:8443/cas/</cas.server>
-		<pwm.server>https://pwm.localdomain.local:8443</pwm.server>
+        <pwm.server>https://pwm.localdomain.local:8443</pwm.server>
+        <java.cas.client.config.strategy>WEB_XML</java.cas.client.config.strategy>
+        <java.cas.client.config.location>/etc/java-cas-client.properties</java.cas.client.config.location>
     </properties>
 
     <profiles>
         <profile>
-            <id>notests</id>
+            <id>skip-all</id>
+            <properties>
+                <maven.javadoc.skip>true</maven.javadoc.skip>
+                <source.skip>true</source.skip>
+                <jspc.skip>true</jspc.skip>
+                <skipTests>true</skipTests>
+                <checkstyle.skip>true</checkstyle.skip>
+                <skip.npm>true</skip.npm>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-tests</id>
             <properties>
                 <skipTests>true</skipTests>
             </properties>
         </profile>
         <profile>
-            <id>nojavadocs</id>
+            <id>skip-javadoc</id>
             <properties>
                 <maven.javadoc.skip>true</maven.javadoc.skip>
             </properties>
         </profile>
+        <profile>
+            <id>skip-checkstyle</id>
+            <properties>
+                <checkstyle.skip>true</checkstyle.skip>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-frontend</id>
+            <properties>
+                <skip.npm>true</skip.npm>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-jspc</id>
+            <properties>
+                <jspc.skip>true</jspc.skip>
+            </properties>
+        </profile>
         <profile>
             <id>doclint-java8-disable</id>
             <activation>
@@ -89,15 +120,6 @@
                     </plugin>
                 </plugins>
             </build>
-        </profile>
-        <profile>
-            <!-- Disables source and javadoc jar file creation.  Handy when wanting to run quick development builds -->
-            <id>developer</id>
-            <properties>
-                <maven.javadoc.skip>true</maven.javadoc.skip>
-                <source.skip>true</source.skip>
-                <jspc.skip>true</jspc.skip>
-            </properties>
         </profile>
 		<!-- Enables CAS configuration -->
 		<profile>
@@ -157,6 +179,43 @@
 				</plugins>
 			</build>
 		</profile>
+		<profile>
+			<id>cas-fileconfig</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>com.google.code.maven-replacer-plugin</groupId>
+						<artifactId>maven-replacer-plugin</artifactId>
+						<version>1.3.5</version>
+						<executions>
+							<execution>
+								<id>replace-webxml-config</id>
+								<phase>prepare-package</phase>
+								<goals>
+									<goal>replace</goal>
+								</goals>
+								<configuration>
+									<includes>
+										<include>src/main/webapp/WEB-INF/web.xml</include>
+									</includes>
+									<replacements>
+										<replacement>
+                                            <token><![CDATA[<!-- cas-config-web.xml start -->]]></token>
+											<value><![CDATA[<!-- cas-config-web.xml start ]]></value>
+										</replacement>
+										<replacement>
+                                            <token><![CDATA[<!-- cas-config-web.xml end -->]]></token>
+											<value><![CDATA[ cas-config-web.xml end -->]]></value>
+										</replacement>
+
+									</replacements>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
     </profiles>
 
     <build>
@@ -230,6 +289,17 @@
                 <configuration>
                     <archiveClasses>true</archiveClasses>
                     <packagingExcludes>WEB-INF/classes</packagingExcludes>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
                 </configuration>
             </plugin>
             <plugin>
@@ -278,6 +348,21 @@
                             </resources>
                         </configuration>
                     </execution>
+                    <execution>
+                        <id>stage-angular</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.basedir}/target/angular</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/angular</directory>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
                     <execution>
                         <id>copy-angular-resources</id>
                         <phase>prepare-package</phase>
@@ -288,7 +373,7 @@
                             <outputDirectory>${project.basedir}/target/${project.artifactId}-${project.version}/public/resources/app</outputDirectory>
                             <resources>
                                 <resource>
-                                    <directory>src/main/angular/dist</directory>
+                                    <directory>${project.basedir}/target/angular/dist</directory>
                                 </resource>
                             </resources>
                         </configuration>
@@ -341,13 +426,6 @@
             <plugin>
                 <artifactId>maven-clean-plugin</artifactId>
                 <version>3.0.0</version>
-                <configuration>
-                    <filesets>
-                        <fileset><directory>src/main/angular/dist</directory></fileset>
-                        <fileset><directory>src/main/angular/node_modules</directory></fileset>
-                        <fileset><directory>src/main/webapp/public/resources/app</directory></fileset>
-                    </filesets>
-                </configuration>
                 <executions>
                     <execution>
                         <id>remove-compiled-jsps</id>
@@ -466,7 +544,7 @@
             <plugin> <!-- checks owsp vulnerability database during verify phase -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>1.4.3</version>
+                <version>1.4.5</version>
                 <executions>
                     <execution>
                         <goals>
@@ -483,7 +561,7 @@
                     <nodeVersion>v6.6.0</nodeVersion>
                     <npmVersion>3.10.8</npmVersion>
                     <installDirectory>target</installDirectory>
-                    <workingDirectory>${basedir}/src/main/angular</workingDirectory>
+                    <workingDirectory>${basedir}/target/angular</workingDirectory>
                 </configuration>
                 <executions>
                     <!-- install node & npm -->
@@ -563,6 +641,14 @@
     </reporting>
 
     <dependencies>
+        <!-- dev tool -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.16.16</version>
+            <scope>provided</scope>
+        </dependency>
+
         <!-- Test dependencies -->
         <dependency>
             <groupId>junit</groupId>
@@ -588,6 +674,12 @@
             <version>1.58</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
+            <version>0.9.10</version>
+            <scope>test</scope>
+        </dependency>
 
         <!-- container dependencies -->
         <dependency>
@@ -630,7 +722,7 @@
         <dependency>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
-            <version>0.6.8</version>
+            <version>0.6.9</version>
         </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -655,17 +747,12 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.2</version>
-        </dependency>
-        <dependency>
-            <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
-            <artifactId>concurrentlinkedhashmap-lru</artifactId>
-            <version>1.4.2</version>
+            <version>4.5.3</version>
         </dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
             <artifactId>syslog4j</artifactId>
-            <version>0.9.57</version>
+            <version>0.9.60</version>
         </dependency>
         <dependency>
             <groupId>log4j</groupId>
@@ -680,12 +767,12 @@
         <dependency>
             <groupId>org.glassfish.jersey.containers</groupId>
             <artifactId>jersey-container-servlet</artifactId>
-            <version>2.24</version>
+            <version>2.26-b02</version>
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.media</groupId>
             <artifactId>jersey-media-json-jackson</artifactId>
-            <version>2.24</version>
+            <version>2.26-b02</version>
         </dependency>
         <dependency>
             <groupId>org.jasig.cas.client</groupId>
@@ -700,12 +787,12 @@
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.55</version>
+            <version>1.57</version>
         </dependency>
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>1.55</version>
+            <version>1.57</version>
         </dependency>
         <dependency>
             <groupId>javax.xml</groupId>
@@ -730,7 +817,7 @@
         <dependency>
             <groupId>org.xeustechnologies</groupId>
             <artifactId>jcl-core</artifactId>
-            <version>2.7</version>
+            <version>2.8</version>
         </dependency>
         <dependency>
             <groupId>net.iharder</groupId>
@@ -750,30 +837,42 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.0.2</version>
+            <version>1.0.5</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-nop</artifactId>
-            <version>1.7.21</version>
+            <version>1.7.22</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>webjars-locator-core</artifactId>
+            <version>0.32</version>
         </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>2.5.2</version>
+        </dependency>
+
+
 
         <!-- client webjar dependencies -->
         <!-- changes in client dependencies require updating AppProperty.properties:http.resources.webjarMappings -->
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dojo</artifactId>
-            <version>1.11.2</version>
+            <version>1.12.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dijit</artifactId>
-            <version>1.11.2</version>
+            <version>1.12.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>dojox</artifactId>
-            <version>1.11.2</version>
+            <version>1.12.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
@@ -798,7 +897,7 @@
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>angular</artifactId>
-            <version>1.5.8</version>
+            <version>1.6.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
@@ -808,7 +907,7 @@
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>angular-translate</artifactId>
-            <version>2.13.0</version>
+            <version>2.13.1</version>
         </dependency>
     </dependencies>
 

+ 22 - 0
src/assembly/pwm.xml

@@ -1,3 +1,25 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
 

+ 0 - 1
src/main/angular/images/icons/m_chevron-down.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" /></svg>

+ 0 - 1
src/main/angular/images/icons/m_chevron-up.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z" /></svg>

+ 22 - 0
src/main/angular/images/icons/m_circle-horz-menu.svg

@@ -1 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m</title><path d="M16,43a6.89,6.89,0,1,1,6.89-6.89A6.9,6.9,0,0,1,16,43Zm0-11.09a4.19,4.19,0,1,0,4.19,4.19A4.2,4.2,0,0,0,16,31.91Z" fill="gray"/><path d="M36.22,43a6.89,6.89,0,1,1,6.89-6.89A6.9,6.9,0,0,1,36.22,43Zm0-11.09a4.19,4.19,0,1,0,4.19,4.19A4.2,4.2,0,0,0,36.22,31.91Z" fill="gray"/><path d="M56.09,43A6.89,6.89,0,1,1,63,36.11,6.9,6.9,0,0,1,56.09,43Zm0-11.09a4.19,4.19,0,1,0,4.19,4.19A4.2,4.2,0,0,0,56.09,31.91Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_close.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_close</title><polygon points="60.45 14.55 57.62 11.72 36.08 33.26 14.61 11.78 11.78 14.61 33.26 36.08 11.62 57.72 14.45 60.55 36.08 38.91 57.88 60.7 60.7 57.88 38.91 36.08 60.45 14.55" fill="gray"/></svg>

+ 23 - 0
src/main/angular/images/icons/m_close_thick.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><polygon points="60.45 14.55 57.62 11.72 36.08 33.26 14.61 11.78 11.78 14.61 33.26 36.08 11.62 57.72 14.45 60.55 36.08 38.91 57.88 60.7 60.7 57.88 38.91 36.08 60.45 14.55" fill="gray"/></svg>

+ 23 - 0
src/main/angular/images/icons/m_configure_thin.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M36,46.32A10.37,10.37,0,1,1,46.45,36,10.4,10.4,0,0,1,36,46.32Zm0-17.38a7,7,0,1,0,7.07,7A7.05,7.05,0,0,0,36,28.93Z" fill="gray"/><path d="M54.43,39.26a3.35,3.35,0,0,0-2.52,2.18,16.94,16.94,0,0,1-.75,1.8,3.32,3.32,0,0,0,.23,3.32L55,51.6l-3.24,3.22-5.08-3.6A3.38,3.38,0,0,0,43.37,51a17.06,17.06,0,0,1-1.81.75,3.35,3.35,0,0,0-2.19,2.5l-1,6H33.75l-1-6a3.35,3.35,0,0,0-2.19-2.5A17.16,17.16,0,0,1,28.72,51a3.37,3.37,0,0,0-3.33.23L20.33,54.8l-3.24-3.22,3.59-5a3.34,3.34,0,0,0,.23-3.31,16.86,16.86,0,0,1-.75-1.8,3.36,3.36,0,0,0-2.52-2.18l-6.46-1V33.68l6.46-1a3.36,3.36,0,0,0,2.52-2.18,16.62,16.62,0,0,1,.75-1.8,3.32,3.32,0,0,0-.23-3.31L17,20.25,20.25,17l5.14,3.66a3.38,3.38,0,0,0,3.33.23,17,17,0,0,1,1.81-.75,3.35,3.35,0,0,0,2.19-2.5l1-6.33h4.58l1,6.33a3.35,3.35,0,0,0,2.2,2.5,16.79,16.79,0,0,1,1.81.75,3.38,3.38,0,0,0,3.33-.23L51.85,17l3.24,3.22-3.7,5.13a3.34,3.34,0,0,0-.23,3.31,16.84,16.84,0,0,1,.76,1.8,3.35,3.35,0,0,0,2.52,2.18l6,1v4.55Zm6.79-9.08L55.87,29.1a21,21,0,0,0-.94-2.24l3-4.53a3.47,3.47,0,0,0-.44-4.39l-3.37-3.35a3.51,3.51,0,0,0-4.42-.44l-4.56,3a21.22,21.22,0,0,0-2.26-.93L41.85,10.9a3.49,3.49,0,0,0-3.43-2.8H33.66a3.5,3.5,0,0,0-3.43,2.8l-1.07,5.34a21.23,21.23,0,0,0-2.26.93l-4.56-3a3.51,3.51,0,0,0-4.42.44l-3.37,3.35a3.47,3.47,0,0,0-.44,4.39l3,4.53a20.8,20.8,0,0,0-.94,2.25l-5.37,1.07A3.49,3.49,0,0,0,8,33.59v4.73a3.49,3.49,0,0,0,2.81,3.41l5.36,1.07A21,21,0,0,0,17.15,45l-3,4.53A3.47,3.47,0,0,0,14.56,54l3.37,3.35a3.51,3.51,0,0,0,4.42.44l4.56-3a20.7,20.7,0,0,0,2.26.93L30.23,61a3.5,3.5,0,0,0,3.43,2.8h4.76A3.5,3.5,0,0,0,41.85,61l1.08-5.33a21.24,21.24,0,0,0,2.26-.93l4.56,3a3.51,3.51,0,0,0,4.42-.44L57.53,54A3.47,3.47,0,0,0,58,49.58l-3-4.53a20.64,20.64,0,0,0,.94-2.25l5.36-1.07A3.49,3.49,0,0,0,64,38.32V33.59A3.49,3.49,0,0,0,61.23,30.17Z" fill="gray" fill-rule="evenodd"/></svg>

+ 2 - 12
src/main/webapp/public/resources/text/eula.html → src/main/angular/images/icons/m_down_thick.svg

@@ -1,10 +1,9 @@
-<!DOCTYPE html>
 <!--
   ~ Password Management Servlets (PWM)
   ~ http://www.pwm-project.org
   ~
   ~ Copyright (c) 2006-2009 Novell, Inc.
-  ~ Copyright (c) 2009-2016 The PWM Project
+  ~ Copyright (c) 2009-2017 The PWM Project
   ~
   ~ This program is free software; you can redistribute it and/or modify
   ~ it under the terms of the GNU General Public License as published by
@@ -21,13 +20,4 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<html>
-<head>
-    <title>EULA</title>
-</head>
-<body>
-<pre>
-EULA TEXT
-</pre>
-</body>
-</html>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><polygon points="35.85 53.38 7.68 25.2 10.5 22.38 35.85 47.72 61.41 22.15 64.24 24.98 35.85 53.38" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_edit.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_edit</title><path d="M26.4,41.73L23.8,47.26a0.61,0.61,0,0,0,.58.82l0.17,0,5.6-2.5a0.62,0.62,0,0,0,.27-0.16L63.2,12.73a0.61,0.61,0,0,0,0-.86l-3-3.14a0.61,0.61,0,0,0-.44-0.19,0.51,0.51,0,0,0-.44.18L53,15H13.53a4,4,0,0,0-4,4V61.2a4,4,0,0,0,4,4H54.38a4,4,0,0,0,4-4v-39l-4,4.06V61.19H13.55l0-42.15H49.05L26.54,41.5A0.61,0.61,0,0,0,26.4,41.73Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_magnify.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9.5,3C13.09,3 16,5.91 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16C5.91,16 3,13.09 3,9.5C3,5.91 5.91,3 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></svg>

+ 0 - 25
src/main/angular/images/icons/m_next-right.svg

@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="72px"
-	 height="72px" viewBox="109 487 72 72" style="enable-background:new 109 487 72 72;" xml:space="preserve">
-<style type="text/css">
-	.st0{fill:#808080;}
-	.st1{fill:#3DD77A;}
-	.st2{fill:#E50000;}
-	.st3{fill:#FFBC00;}
-	.st4{fill:#FF8578;}
-	.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#FF8575;}
-	.st6{fill-rule:evenodd;clip-rule:evenodd;fill:#808080;}
-	.st7{fill:none;stroke:#808080;stroke-width:4;stroke-miterlimit:10;}
-	.st8{display:none;}
-	.st9{display:inline;fill:none;}
-</style>
-<g id="SVG_icons">
-	<g id="right">
-		<polygon class="st0" points="133.77,551.12 130.94,548.29 156.5,522.73 131.16,497.38 133.99,494.56 162.16,522.73 		"/>
-	</g>
-</g>
-<g id="Rectangles" class="st8">
-	<rect x="109.36" y="487.36" class="st9" width="71.28" height="71.28"/>
-</g>
-</svg>

+ 22 - 0
src/main/angular/images/icons/m_orgchart.svg

@@ -1 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_brand</title><path d="M65,43.46H55.4V35.75a1,1,0,0,0-1-1H37.48V28.36H47.92a1.5,1.5,0,0,0,1.5-1.5V8.91a1.5,1.5,0,0,0-1.5-1.5H25.18a1.5,1.5,0,0,0-1.5,1.5V26.86a1.5,1.5,0,0,0,1.5,1.5H35.48v6.39H18.55a1,1,0,0,0-1,1v7.71H7.93A1.5,1.5,0,0,0,6.43,45V62.9a1.5,1.5,0,0,0,1.5,1.5H29.17a1.5,1.5,0,0,0,1.5-1.5V45a1.5,1.5,0,0,0-1.5-1.5H19.55V36.75H53.4v6.71H43.78a1.5,1.5,0,0,0-1.5,1.5V62.9a1.5,1.5,0,0,0,1.5,1.5H65a1.5,1.5,0,0,0,1.5-1.5V45A1.5,1.5,0,0,0,65,43.46Zm-38.34-33H46.42V25.36H26.68V10.41Zm1,51H9.43V46.46H27.67V61.4Zm35.85,0H45.28V46.46H63.52V61.4Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_question_mark.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_question_mark</title><path d="M32.31,47.66l-0.16-2c-0.47-4.12.93-8.79,4.82-13.37,3.5-4.2,5.44-7.23,5.44-10.73,0-4-2.49-6.61-7.39-6.69a13.82,13.82,0,0,0-7.85,2.41l-1.87-4.9A20.58,20.58,0,0,1,36.43,9.24c8.94,0,13,5.52,13,11.43,0,5.29-3,9.18-6.69,13.53-3.42,4-4.67,7.54-4.43,11.51l0.08,1.94H32.31Zm2.8,15.79a4.54,4.54,0,0,1-4.51-4.82,4.6,4.6,0,0,1,4.67-4.9c2.72,0,4.59,2,4.59,4.9a4.53,4.53,0,0,1-4.67,4.82H35.11Z" fill="gray"/><rect x="0.36" y="0.36" width="71.28" height="71.28" fill="none"/></svg>

+ 23 - 0
src/main/angular/images/icons/m_search_thick.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M64.5,61.57,46.4,42.7a20.11,20.11,0,1,0-2.88,2.77l18.1,18.87ZM15.16,29.77a16,16,0,1,1,16,16A16,16,0,0,1,15.16,29.77Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_settings.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,15.5C10.07,15.5 8.5,13.93 8.5,12C8.5,10.07 10.07,8.5 12,8.5C13.93,8.5 15.5,10.07 15.5,12C15.5,13.93 13.93,15.5 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>

+ 23 - 0
src/main/angular/images/icons/m_up_thick.svg

@@ -0,0 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><polygon points="61.58 51.96 36.01 26.4 10.67 51.74 7.84 48.91 36.01 20.74 64.41 49.13 61.58 51.96" fill="gray"/></svg>

+ 22 - 0
src/main/angular/images/icons/m_view-list.svg

@@ -1 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M63,17.65H8.9a1.5,1.5,0,0,1-1.5-1.5V9a1.5,1.5,0,0,1,1.5-1.5H63A1.5,1.5,0,0,1,64.49,9v7.14A1.5,1.5,0,0,1,63,17.65Zm-52.59-3H61.49V10.51H10.4v4.14Z" fill="gray"/><path d="M63,64.45H8.9A1.5,1.5,0,0,1,7.4,63V55.81a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,63,64.45Zm-52.59-3H61.49V57.31H10.4v4.14Z" fill="gray"/><path d="M63,48.85H8.9a1.5,1.5,0,0,1-1.5-1.5V40.21a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5v7.14A1.5,1.5,0,0,1,63,48.85Zm-52.59-3H61.49V41.71H10.4v4.14Z" fill="gray"/><path d="M63,33.25H8.9a1.5,1.5,0,0,1-1.5-1.5V24.61a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5v7.14A1.5,1.5,0,0,1,63,33.25Zm-52.59-3H61.49V26.11H10.4v4.14Z" fill="gray"/></svg>

+ 22 - 0
src/main/angular/images/icons/m_view-tile.svg

@@ -1 +1,23 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M31.35,32.85H8.76a1.5,1.5,0,0,1-1.5-1.5V8.91a1.5,1.5,0,0,1,1.5-1.5H31.35a1.5,1.5,0,0,1,1.5,1.5V31.35A1.5,1.5,0,0,1,31.35,32.85Zm-21.09-3H29.85V10.41H10.26V29.85Z" fill="gray"/><path d="M63,32.85H40.41a1.5,1.5,0,0,1-1.5-1.5V8.91a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V31.35A1.5,1.5,0,0,1,63,32.85Zm-21.09-3H61.5V10.41H41.91V29.85Z" fill="gray"/><path d="M31.35,64.5H8.76A1.5,1.5,0,0,1,7.26,63V40.56a1.5,1.5,0,0,1,1.5-1.5H31.35a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,31.35,64.5Zm-21.09-3H29.85V42.06H10.26V61.5Z" fill="gray"/><path d="M63,64.5H40.41a1.5,1.5,0,0,1-1.5-1.5V40.56a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,63,64.5Zm-21.09-3H61.5V42.06H41.91V61.5Z" fill="gray"/></svg>

+ 22 - 1
src/main/angular/index.html

@@ -1,7 +1,28 @@
 <!DOCTYPE html>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <html lang="en">
 <head>
-    <base href="/" />
     <meta charset="UTF-8">
     <meta name="viewport" content="initial-scale=1, maximum-scale=1">
     <title>SSPR Development</title>

+ 1 - 1
src/main/angular/karma.conf.js

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 5 - 1
src/main/angular/src/main.dev.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,7 +25,9 @@ import { bootstrap, module } from 'angular';
 import ConfigService from './services/config.service.dev';
 import peopleSearchModule from './peoplesearch/peoplesearch.module';
 import PeopleService from './services/people.service.dev';
+import PwmService from './services/pwm.service.dev';
 import routes from './routes';
+import routeErrorHandler from './route-error-handler';
 import uiRouter from 'angular-ui-router';
 
 // fontgen-loader needs this :(
@@ -43,7 +45,9 @@ module('app', [
         $translateProvider.useSanitizeValueStrategy('escapeParameters');
         $translateProvider.preferredLanguage('en');
     }])
+    .run(routeErrorHandler)
     .service('PeopleService', PeopleService)
+    .service('PwmService', PwmService)
     .service('ConfigService', ConfigService);
 
 // Attach to the page document

+ 7 - 3
src/main/angular/src/main.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import peopleSearchModule from './peoplesearch/peoplesearch.module';
 import PeopleService from './services/people.service';
 import PwmService from './services/pwm.service';
 import routes from './routes';
+import routeErrorHandler from './route-error-handler';
 import TranslationsLoaderFactory from './services/translations-loader.factory';
 import uiRouter from 'angular-ui-router';
 
@@ -51,11 +52,14 @@ module('app', [
                 .fallbackLanguage('fallback')
                 .forceAsyncReload(true);
         }])
+    .run(routeErrorHandler)
     .service('PeopleService', PeopleService)
     .service('PwmService', PwmService)
     .service('ConfigService', ConfigService)
     .factory('translationsLoader', TranslationsLoaderFactory);
 
-// Attach to the page document
-bootstrap(document, ['app'], { strictDi: true });
+// Attach to the page document, wait for PWM to load first
+window['PWM_GLOBAL'].startupFunctions.push(() => {
+    bootstrap(document, ['app'], { strictDi: true });
+});
 

+ 1 - 1
src/main/angular/src/models/column.model.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 5 - 5
src/main/angular/src/models/orgchart-data.model.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,10 +21,10 @@
  */
 
 
-import Person from './person.model';
+import { IPerson } from './person.model';
 export default class OrgChartData {
 
-    constructor(public manager: Person,
-                public children: Person[],
-                public self: Person) {}
+    constructor(public manager: IPerson,
+                public children: IPerson[],
+                public self: IPerson) {}
 }

+ 14 - 35
src/main/angular/src/models/person.model.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,45 +21,24 @@
  */
 
 
-export default class Person {
+export interface IPerson {
     // Common properties
-    userKey: string;
-    numDirectReports: number;
+    userKey?: string;
+    numDirectReports?: number;
 
     // Autocomplete properties (via Search)
-    _displayName: string;
+    _displayName?: string;
 
     // Details properties (not available in search)
-    detail: any;
-    displayNames: string[];
-    photoURL: string;
-    links: any[];
+    detail?: any;
+    displayNames?: string[];
+    photoURL?: string;
+    links?: any[];
 
     // Search properties (not available in details)
-    givenName: string;
-    mail: string;
-    sn: string;
-    telephoneNumber: string;
-    title: string;
-
-    constructor(options: any) {
-        // Common properties
-        this.userKey = options.userKey;
-
-        // Autocomplete properties (via Search)
-        this._displayName = options._displayName;
-
-        // Details properties
-        this.detail = options.detail;
-        this.displayNames = options.displayNames;
-        this.photoURL = options.photoURL;
-        this.links = options.links;
-
-        // Search properties
-        this.givenName = options.givenName;
-        this.mail = options.mail;
-        this.sn = options.sn;
-        this.telephoneNumber = options.telephoneNumber;
-        this.title = options.title;
-    }
+    givenName?: string;
+    mail?: string;
+    sn?: string;
+    telephoneNumber?: string;
+    title?: string;
 }

+ 4 - 4
src/main/angular/src/models/search-result.model.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,14 +21,14 @@
  */
 
 
-import Person from './person.model';
+import { IPerson } from './person.model';
 
 export default class SearchResult {
     sizeExceeded: boolean;
-    people: Person[];
+    people: IPerson[];
 
     constructor(options: any) {
         this.sizeExceeded = options.sizeExceeded;
-        this.people = options.searchResults.map((person: any) => new Person(person));
+        this.people = options.searchResults.map((person: any) => <IPerson>(person));
     }
 }

+ 31 - 4
src/main/angular/src/peoplesearch/orgchart-search.component.html

@@ -1,8 +1,31 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <mf-app-bar>
-    <div class="page-content-title" translate="Title_Organization">Organization</div>
+    <div id="page-content-title" translate="Title_Organization">Organization</div>
     <mf-auto-complete search-text="$ctrl.query"
                       on-search-text-change="$ctrl.onSearchTextChange(value)"
                       search="$ctrl.autoCompleteSearch(query)"
+                      input-debounce="$ctrl.inputDebounce"
                       item-selected="$ctrl.onAutoCompleteItemSelected(person)"
                       item="person">
         <content-template>
@@ -13,20 +36,24 @@
     <mf-icon-button
             icon="view-tile"
             ng-click="$ctrl.gotoSearchState('search.cards')"
-            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"
+            id="view-tile-icon"></mf-icon-button>
     <mf-icon-button
             icon="view-list"
             ng-click="$ctrl.gotoSearchState('search.table')"
-            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"
+            id="view-list-icon"></mf-icon-button>
     <div class="mf-divider vertical"></div>
     <mf-icon-button
             icon="orgchart"
             disabled="true"
-            ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
+            ng-attr-title="{{ 'Title_OrgChart' | translate }}"
+            id="orgcharg-icon"></mf-icon-button>
 </mf-app-bar>
 
 <org-chart person="$ctrl.person"
            direct-reports="$ctrl.directReports"
+           show-images="$ctrl.photosEnabled"
            management-chain="$ctrl.managementChain">
 </org-chart>
 

+ 2 - 2
src/main/angular/src/peoplesearch/orgchart-search.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 84 - 35
src/main/angular/src/peoplesearch/orgchart-search.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,68 +22,99 @@
 
 
 import { Component } from '../component';
-import { isArray, isString, IPromise, IQService, IScope } from 'angular';
+import { IConfigService } from '../services/config.service';
 import { IPeopleService } from '../services/people.service';
-import Person from '../models/person.model';
+import IPwmService from '../services/pwm.service';
+import { isArray, isString, IPromise, IQService, IScope } from 'angular';
+import LocalStorageService from '../services/local-storage.service';
 import OrgChartData from '../models/orgchart-data.model';
+import { IPerson } from '../models/person.model';
 
 @Component({
     stylesheetUrl: require('peoplesearch/orgchart-search.component.scss'),
     templateUrl: require('peoplesearch/orgchart-search.component.html')
 })
 export default class OrgChartSearchComponent {
-    directReports: Person[];
-    managementChain: Person[];
-    person: Person;
+    directReports: IPerson[];
+    inputDebounce: number;
+    managementChain: IPerson[];
+    person: IPerson;
+    photosEnabled: boolean;
     query: string;
+    searchTextLocalStorageKey: string;
 
-    static $inject = [ '$q', '$scope', '$state', '$stateParams', 'PeopleService' ];
+    static $inject = [ '$q',
+        '$scope',
+        '$state',
+        '$stateParams',
+        'ConfigService',
+        'LocalStorageService',
+        'PeopleService',
+        'PwmService'
+    ];
     constructor(private $q: IQService,
                 private $scope: IScope,
                 private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
-                private peopleService: IPeopleService) {
+                private configService: IConfigService,
+                private localStorageService: LocalStorageService,
+                private peopleService: IPeopleService,
+                private pwmService: IPwmService) {
+        this.searchTextLocalStorageKey = this.localStorageService.keys.SEARCH_TEXT;
+        this.inputDebounce = this.pwmService.ajaxTypingWait;
     }
 
     $onInit(): void {
         const self = this;
 
-        // Read query from state parameters
-        const queryParameter = this.$stateParams['query'];
-        // If multiple query parameters are defined, use the first one
-        if (isArray(queryParameter)) {
-            this.query = queryParameter[0].trim();
-        }
-        else if (isString(queryParameter)) {
-            this.query = queryParameter.trim();
-        }
+        this.configService.photosEnabled().then(
+            (photosEnabled: boolean) => {
+                this.photosEnabled = photosEnabled;
+            });
+
+        this.query = this.getSearchText();
 
         let personId: string = this.$stateParams['personId'];
 
         this.fetchOrgChartData(personId)
             .then((orgChartData: OrgChartData) => {
+                if (!orgChartData) {
+                    return;
+                }
+
                 // Override personId in case it was undefined
                 personId = orgChartData.self.userKey;
 
-                self.$q.all({
-                    directReports: self.peopleService.getDirectReports(personId),
-                    managementChain: self.peopleService.getManagementChain(personId),
-                    person: self.peopleService.getPerson(personId)
-                })
-                .then((data) => {
-                    self.$scope.$evalAsync(() => {
-                        self.directReports = data['directReports'];
-                        self.managementChain = data['managementChain'];
-                        self.person = data['person'];
-                    });
-                })
-                .catch(() => {
-                    // TODO: error handling
-                });
+                self.peopleService.getPerson(personId)
+                    .then((person: IPerson) => {
+                            self.person = person;
+                        },
+                        (error) => {
+                            // TODO: handle error
+                        });
+
+                self.peopleService.getManagementChain(personId)
+                    .then((managementChain: IPerson[]) => {
+                            self.managementChain = managementChain;
+                        },
+                        (error) => {
+                            // TODO: handle error
+                        });
+
+                self.peopleService.getDirectReports(personId)
+                    .then((directReports: IPerson[]) => {
+                            self.directReports = directReports;
+                        },
+                        (error) => {
+                            // TODO: handle error
+                        });
+            },
+            (error) => {
+                // TODO: handle error
             });
     }
 
-    autoCompleteSearch(query: string): IPromise<Person[]> {
+    autoCompleteSearch(query: string): IPromise<IPerson[]> {
         return this.peopleService.autoComplete(query);
     }
 
@@ -91,15 +122,33 @@ export default class OrgChartSearchComponent {
         this.$state.go(state, { query: this.query });
     }
 
-    onAutoCompleteItemSelected(person: Person): void {
+    onAutoCompleteItemSelected(person: IPerson): void {
         this.$state.go('orgchart.search', { personId: person.userKey, query: null });
     }
 
     onSearchTextChange(value: string): void {
         this.query = value;
+        this.storeSearchText();
     }
 
     private fetchOrgChartData(personId): IPromise<OrgChartData> {
-        return this.peopleService.getOrgChartData(personId);
+        return this.peopleService.getOrgChartData(personId, true);
+    }
+
+    private getSearchText(): string {
+        let param: string = this.$stateParams['query'];
+        // If multiple query parameters are defined, use the first one
+        if (isArray(param)) {
+            param = param[0].trim();
+        }
+        else if (isString(param)) {
+            param = param.trim();
+        }
+
+        return param || this.localStorageService.getItem(this.searchTextLocalStorageKey);
+    }
+
+    protected storeSearchText(): void {
+        this.localStorageService.setItem(this.searchTextLocalStorageKey, this.query || '');
     }
 }

+ 28 - 9
src/main/angular/src/peoplesearch/orgchart.component.html

@@ -1,24 +1,41 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <div class="org-chart-section managers"
      ng-class="{ 'overflow': $ctrl.showingOverflow() }"
-     ng-if="$ctrl.hasManagementChain() || $ctrl.isPersonOrphan()">
+     ng-if="$ctrl.hasManagementChain()">
     <h3 translate="Title_Management">Management</h3>
-    <div ng-if="!$ctrl.isPersonOrphan()">
+    <div>
         <div class="manager"
              ng-repeat="manager in $ctrl.getManagementChain() track by $id(manager.displayNames)">
             <div class="org-chart-connector"></div>
             <person-card person="manager"
+                         show-image="$ctrl.showImages || ($ctrl.showingOverflow() && $last)"
                          size="{{ $ctrl.getManagerCardSize() }}"
                          show-direct-report-count="false"
                          ng-click="$ctrl.selectPerson(manager.userKey)">
             </person-card>
         </div>
     </div>
-    <div ng-if="$ctrl.isPersonOrphan()">
-        <div class="manager empty-manager">
-            <div class="org-chart-connector"></div>
-            <person-card person="$ctrl.emptyPerson" size="{{ $ctrl.getManagerCardSize() }}"></person-card>
-        </div>
-    </div>
 </div>
 
 <div class="org-chart-section">
@@ -26,7 +43,8 @@
                  direct-reports="$ctrl.directReports"
                  ng-click="$ctrl.onClickPerson()"
                  size="large"
-                 show-direct-report-count="true"></person-card>
+                 show-direct-report-count="true"
+                 show-image="$ctrl.showImages"></person-card>
 </div>
 
 <div class="org-chart-section direct-reports" ng-if="$ctrl.hasDirectReports()">
@@ -36,6 +54,7 @@
     <div class="person-card-list">
         <person-card person="directReport"
                      show-direct-report-count="false"
+                     show-image="$ctrl.showImages"
                      ng-repeat="directReport in $ctrl.directReports"
                      ng-click="$ctrl.selectPerson(directReport.userKey)">
         </person-card>

+ 14 - 5
src/main/angular/src/peoplesearch/orgchart.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -58,7 +58,7 @@ org-chart {
   // (L) Wide enough to show main person offset to the right. Manager should now be locked in place (instead of centered)
   &.large {
     > .org-chart-section {
-      text-align: start;
+      text-align: left;
 
       > person-card {
         &[size="large"] {
@@ -79,7 +79,7 @@ org-chart {
         .manager {
           display: block;
           margin-left: 135px;
-          text-align: start;
+          text-align: left;
         }
       }
     }
@@ -229,7 +229,7 @@ org-chart {
       line-height: 14px;
       margin: 0;
       padding: 15px 0 5px 0;
-      text-align: start;
+      text-align: left;
     }
 
     > person-card {
@@ -268,6 +268,12 @@ org-chart {
 [dir="rtl"] {
   // (XS) Default display
   org-chart {
+    > .org-chart-section {
+      > h3 {
+        text-align: right;
+      }
+    }
+
     // (S) Too wide for full width person-card in direct reports
     &.small {
       > .org-chart-section {
@@ -283,6 +289,8 @@ org-chart {
     // (L) Wide enough to show main person offset to the right. Manager should now be locked in place (instead of centered)
     &.large {
       > .org-chart-section {
+        text-align: right;
+
         > person-card {
           &[size="large"] {
             margin: 0 128px 0 0;
@@ -301,6 +309,7 @@ org-chart {
           }
 
           .manager {
+            text-align: right;
             margin-left: auto;
             margin-right: 135px;
           }

+ 1 - 1
src/main/angular/src/peoplesearch/orgchart.component.test.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 13 - 22
src/main/angular/src/peoplesearch/orgchart.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,7 +24,7 @@
 import { Component } from '../component';
 import { element, IAugmentedJQuery, IFilterService, IScope, IWindowService } from 'angular';
 import ElementSizeService from '../ux/element-size.service';
-import Person from '../models/person.model';
+import { IPerson } from '../models/person.model';
 
 export enum OrgChartSize {
     ExtraSmall = 0,
@@ -38,29 +38,22 @@ export enum OrgChartSize {
     bindings: {
         directReports: '<',
         managementChain: '<',
-        person: '<'
+        person: '<',
+        showImages: '<'
     },
     stylesheetUrl: require('peoplesearch/orgchart.component.scss'),
     templateUrl: require('peoplesearch/orgchart.component.html')
 })
 export default class OrgChartComponent {
-    directReports: Person[];
+    directReports: IPerson[];
     elementWidth: number;
     isExtraLargeLayout: boolean;
-    managementChain: Person[];
-    person: Person;
-
-    emptyPerson: Person = new Person({
-        displayNames: [
-            'No Managers'
-        ],
-        photoURL: null,
-        userKey: null
-    });
+    managementChain: IPerson[];
+    person: IPerson;
 
     private elementSize: OrgChartSize = OrgChartSize.ExtraSmall;
     private maxVisibleManagers: number;
-    private visibleManagers: Person[];
+    private visibleManagers: IPerson[];
 
     static $inject = [ '$element', '$filter', '$scope', '$state', '$window', 'MfElementSizeService' ];
     constructor(
@@ -95,7 +88,7 @@ export default class OrgChartComponent {
         return this.isExtraLargeLayout ? 'small' : 'normal';
     }
 
-    getManagementChain(): Person[] {
+    getManagementChain(): IPerson[] {
         // Display managers in a row
         if (this.isExtraLargeLayout) {
             // All managers can fit on screen
@@ -110,7 +103,7 @@ export default class OrgChartComponent {
                 this.visibleManagers = this.managementChain.slice(0, this.maxVisibleManagers - 1);
                 const lastManager = this.managementChain[this.maxVisibleManagers - 2];
 
-                this.visibleManagers.push(new Person({
+                this.visibleManagers.push(<IPerson>({
                     userKey: lastManager.userKey,
                     photoURL: null,
                     displayNames: []
@@ -132,10 +125,6 @@ export default class OrgChartComponent {
         return this.managementChain && !!this.managementChain.length;
     }
 
-    isPersonOrphan(): boolean {
-        return !(this.hasDirectReports() || this.hasManagementChain());
-    }
-
     onClickPerson(): void {
         if (this.person) {
             this.$state.go('orgchart.search.details', { personId: this.person.userKey });
@@ -154,7 +143,9 @@ export default class OrgChartComponent {
 
     private onResize(newValue: number): void {
         this.isExtraLargeLayout = (newValue >= OrgChartSize.ExtraLarge);
-
+        if (!this.isExtraLargeLayout) {
+            this.resetManagerList();
+        }
         this.maxVisibleManagers = Math.floor(
          (newValue - 115 /* left margin */) / 125 /* card width + right margin */);
     }

+ 144 - 50
src/main/angular/src/peoplesearch/peoplesearch-base.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,29 +21,53 @@
  */
 
 
-import { IPeopleService } from '../services/people.service';
 import { isArray, isString, IPromise, IQService, IScope } from 'angular';
-import Person from '../models/person.model';
+import { IConfigService } from '../services/config.service';
+import { IPeopleService } from '../services/people.service';
+import IPwmService from '../services/pwm.service';
+import LocalStorageService from '../services/local-storage.service';
+import { IPerson } from '../models/person.model';
+import PromiseService from '../services/promise.service';
 import SearchResult from '../models/search-result.model';
 
+const SEARCH_TEXT_LOCAL_STORAGE_KEY = 'searchText';
+
 abstract class PeopleSearchBaseComponent {
-    loading: boolean;
-    query: string;
-    searchMessage: (string | IPromise<string>);
+    errorMessage: string;
+    inputDebounce: number;
+    orgChartEnabled: boolean;
+    protected pendingRequests: IPromise<any>[] = [];
+    searchMessage: string;
     searchResult: SearchResult;
+    query: string;
+    searchTextLocalStorageKey: string;
+    searchViewLocalStorageKey: string;
 
     constructor(protected $q: IQService,
-                          protected $scope: IScope,
-                          protected $state: angular.ui.IStateService,
-                          protected $stateParams: angular.ui.IStateParamsService,
-                          protected $translate: angular.translate.ITranslateService,
-                          protected peopleService: IPeopleService) {}
+                protected $scope: IScope,
+                protected $state: angular.ui.IStateService,
+                protected $stateParams: angular.ui.IStateParamsService,
+                protected $translate: angular.translate.ITranslateService,
+                protected configService: IConfigService,
+                protected localStorageService: LocalStorageService,
+                protected peopleService: IPeopleService,
+                protected promiseService: PromiseService,
+                protected pwmService: IPwmService) {
+        this.searchTextLocalStorageKey = this.localStorageService.keys.SEARCH_TEXT;
+        this.searchViewLocalStorageKey = this.localStorageService.keys.SEARCH_VIEW;
+
+        this.inputDebounce = this.pwmService.ajaxTypingWait;
+    }
+
+    getMessage(): string {
+        return this.errorMessage || this.searchMessage;
+    }
 
     gotoOrgchart(): void {
         this.gotoState('orgchart.index');
     }
 
-    gotoState(state: string): void {
+    private gotoState(state: string): void {
         this.$state.go(state, { query: this.query });
     }
 
@@ -61,36 +85,67 @@ abstract class PeopleSearchBaseComponent {
         }
 
         this.query = value;
-        this.setSearchMessage(null);
+
+        this.storeSearchText();
+        this.clearSearchMessage();
+        this.clearErrorMessage();
         this.fetchData();
     }
 
-    selectPerson(person: Person): void {
+    selectPerson(person: IPerson): void {
         this.$state.go('.details', { personId: person.userKey, query: this.query });
     }
 
-    protected setSearchMessage(message: (string | IPromise<string>)) {
-        if (!message) {
-            this.clearSearchMessage();
-            return;
+    get loading(): boolean {
+        return !!this.pendingRequests.length;
+    }
+
+    protected abortPendingRequests() {
+        for (let index = 0; index < this.pendingRequests.length; index++) {
+            let pendingRequest = this.pendingRequests[index];
+            this.promiseService.abort(pendingRequest);
         }
 
-        if (typeof message === 'string') {
-            this.searchMessage = message;
+        this.pendingRequests = [];
+    }
+
+    protected removePendingRequest(promise: IPromise<any>) {
+        let index = this.pendingRequests.indexOf(promise);
+
+        if (index > -1) {
+            this.pendingRequests.splice(index, 1);
         }
-        else {
-            const self = this;
+    }
 
-            message.then((translation: string) => {
-                self.searchMessage = translation;
-            });
+    protected setErrorMessage(message: string) {
+        this.errorMessage = message;
+    }
+
+    protected clearErrorMessage() {
+        this.errorMessage = null;
+    }
+
+    // If message is a string it will be translated. If it is a promise it will assign the string from the resolved
+    // promise
+    protected setSearchMessage(translationKey: string) {
+        if (!translationKey) {
+            this.clearSearchMessage();
+            return;
         }
+
+        const self = this;
+        this.$translate(translationKey.toString())
+            .then((translation: string) => {
+            self.searchMessage = translation;
+        });
     }
 
     protected clearSearch(): void {
         this.query = null;
         this.searchResult = null;
+        this.clearErrorMessage();
         this.clearSearchMessage();
+        this.abortPendingRequests();
     }
 
     protected clearSearchMessage(): void  {
@@ -100,47 +155,86 @@ abstract class PeopleSearchBaseComponent {
     abstract fetchData(): void;
 
     protected fetchSearchData(): IPromise<SearchResult> {
-        const self = this;
+        this.abortPendingRequests();
+        this.searchResult = null;
 
         if (!this.query) {
             this.clearSearch();
             return null;
         }
 
-        this.loading = true;
-
-        return this.peopleService
-            .search(this.query)
-            .then((searchResult: SearchResult) => {
-                self.clearSearchMessage();
-
-                // Too many results returned
-                if (searchResult.sizeExceeded) {
-                    self.setSearchMessage(self.$translate('Display_SearchResultsExceeded'));
-                }
-                // No results returned. Not an else if statement so that the more important message is presented
-                if (!searchResult.people.length) {
-                    self.setSearchMessage(self.$translate('Display_SearchResultsNone'));
-                }
+        const self = this;
 
-                return this.$q.resolve(searchResult);
-            })
+        let promise = this.peopleService.search(this.query);
+
+        this.pendingRequests.push(promise);
+
+        return promise
+            .then(
+                (searchResult: SearchResult) => {
+                    self.clearErrorMessage();
+                    self.clearSearchMessage();
+
+                    // Aborted request
+                    if (!searchResult) {
+                        return;
+                    }
+
+                    // Too many results returned
+                    if (searchResult.sizeExceeded) {
+                        self.setSearchMessage('Display_SearchResultsExceeded');
+                    }
+
+                    // No results returned. Not an else if statement so that the more important message is presented
+                    if (!searchResult.people.length) {
+                        self.setSearchMessage('Display_SearchResultsNone');
+                    }
+
+                    return this.$q.resolve(searchResult);
+                },
+                (error) => {
+                    self.setErrorMessage(error);
+                    self.clearSearchMessage();
+                })
             .finally(() => {
-                self.loading = false;
+                self.removePendingRequest(promise);
             });
     }
 
     protected initialize(): void {
-        // Read query from state parameters
-        const queryParameter = this.$stateParams['query'];
+        // Determine whether org-chart should appear
+        this.configService.orgChartEnabled().then((orgChartEnabled: boolean) => {
+            this.orgChartEnabled = orgChartEnabled;
+        });
+
+        this.query = this.getSearchText();
+    }
 
+    private getSearchText(): string {
+        let param: string = this.$stateParams['query'];
         // If multiple query parameters are defined, use the first one
-        if (isArray(queryParameter)) {
-            this.query = queryParameter[0].trim();
+        if (isArray(param)) {
+            param = param[0].trim();
         }
-        else if (isString(queryParameter)) {
-            this.query = queryParameter.trim();
+        else if (isString(param)) {
+            param = param.trim();
         }
+
+        return param || this.localStorageService.getItem(this.searchTextLocalStorageKey);
+    }
+
+    protected storeSearchText(): void {
+        this.localStorageService.setItem(this.searchTextLocalStorageKey, this.query || '');
+    }
+
+    protected toggleView(state: string): void {
+        this.storeSearchView(state);
+        this.storeSearchText();
+        this.gotoState(state);
+    }
+
+    private storeSearchView(state: string) {
+        this.localStorageService.setItem(this.searchViewLocalStorageKey, state);
     }
 }
 

+ 40 - 9
src/main/angular/src/peoplesearch/peoplesearch-cards.component.html

@@ -1,6 +1,29 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <mf-app-bar>
-    <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
-    <mf-search-bar search-text="$ctrl.query"
+    <div id="page-content-title" translate="Title_PeopleSearch">People Search</div>
+    <mf-search-bar input-debounce="$ctrl.inputDebounce"
+                   search-text="$ctrl.query"
                    on-search-text-change="$ctrl.onSearchTextChange(value)"
                    on-key-down="$ctrl.onSearchBoxKeyDown($event)"
                    auto-focus></mf-search-bar>
@@ -8,25 +31,33 @@
     <mf-icon-button
             icon="view-tile"
             disabled="true"
-            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"
+            id="view-tile-icon"></mf-icon-button>
     <mf-icon-button
             icon="view-list"
             ng-click="$ctrl.gotoTableView()"
-            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
-    <div class="mf-divider vertical"></div>
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"
+            id="view-list-icon"></mf-icon-button>
+    <div class="mf-divider vertical" ng-if="$ctrl.orgChartEnabled"></div>
     <mf-icon-button
             icon="orgchart"
             ng-click="$ctrl.gotoOrgchart()"
-            ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
+            ng-if="$ctrl.orgChartEnabled"
+            ng-attr-title="{{ 'Title_OrgChart' | translate }}"
+            id="orgchart-icon"></mf-icon-button>
 </mf-app-bar>
 
 <div class="people-search-component-content">
-    <div class="search-info"
-         ng-if="$ctrl.loading || $ctrl.searchMessage"
-         ng-bind="$ctrl.loading ? ('Display_PleaseWait' | translate) : $ctrl.searchMessage"></div>
+    <div class="search-info-container">
+        <div class="search-info"
+             ng-if="$ctrl.loading || $ctrl.searchMessage || $ctrl.errorMessage"
+             ng-bind="$ctrl.getMessage() || ('Display_PleaseWait' | translate)">
+        </div>
+    </div>
 
     <div class="person-card-list">
         <person-card person="person"
+                     show-image="$ctrl.photosEnabled"
                      ng-repeat="person in $ctrl.searchResult.people"
                      ng-click="$ctrl.selectPerson(person)">
         </person-card>

+ 5 - 3
src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -42,7 +42,7 @@ people-search-cards {
   &.large {
     > .people-search-component-content {
       > .person-card-list {
-        text-align: start;
+        text-align: left;
         margin: 0;
 
         > person-card {
@@ -76,6 +76,8 @@ people-search-cards {
     &.large {
       > .people-search-component-content {
         .person-card-list {
+          text-align: right;
+
           > person-card {
             margin-right: auto;
             margin-left: 5px;

+ 65 - 17
src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,10 +23,14 @@
 
 import { Component } from '../component';
 import ElementSizeService from '../ux/element-size.service';
-import { IAugmentedJQuery, IQService, IScope } from 'angular';
+import IConfigService from '../services/config.service';
 import IPeopleService from '../services/people.service';
+import IPwmService from '../services/pwm.service';
+import { isString, IAugmentedJQuery, IQService, IScope } from 'angular';
+import LocalStorageService from '../services/local-storage.service';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
-import Person from '../models/person.model';
+import { IPerson } from '../models/person.model';
+import PromiseService from '../services/promise.service';
 import SearchResult from '../models/search-result.model';
 
 export enum PeopleSearchCardsSize {
@@ -40,6 +44,8 @@ export enum PeopleSearchCardsSize {
     templateUrl: require('peoplesearch/peoplesearch-cards.component.html')
 })
 export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponent {
+    photosEnabled: boolean;
+
     static $inject = [
         '$element',
         '$q',
@@ -47,8 +53,12 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
         '$state',
         '$stateParams',
         '$translate',
+        'ConfigService',
+        'LocalStorageService',
         'MfElementSizeService',
-        'PeopleService'
+        'PeopleService',
+        'PromiseService',
+        'PwmService'
     ];
     constructor(private $element: IAugmentedJQuery,
                 $q: IQService,
@@ -56,9 +66,22 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
+                configService: IConfigService,
+                localStorageService: LocalStorageService,
                 private elementSizeService: ElementSizeService,
-                peopleService: IPeopleService) {
-        super($q, $scope, $state, $stateParams, $translate, peopleService);
+                peopleService: IPeopleService,
+                promiseService: PromiseService,
+                pwmService: IPwmService) {
+        super($q,
+            $scope,
+            $state,
+            $stateParams,
+            $translate,
+            configService,
+            localStorageService,
+            peopleService,
+            promiseService,
+            pwmService);
     }
 
     $onDestroy(): void {
@@ -69,21 +92,31 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
         this.initialize();
         this.fetchData();
 
+        this.configService.photosEnabled().then((photosEnabled: boolean) => {
+            this.photosEnabled = photosEnabled;
+        });
+
         this.elementSizeService.watchWidth(this.$element, PeopleSearchCardsSize);
     }
 
     gotoTableView() {
-        this.gotoState('search.table');
+        this.toggleView('search.table');
     }
 
     fetchData() {
-        let searchResult = this.fetchSearchData();
-        if (searchResult) {
-            searchResult.then(this.onSearchResult.bind(this));
+        let searchResultPromise = this.fetchSearchData();
+        if (searchResultPromise) {
+
+            searchResultPromise.then(this.onSearchResult.bind(this));
         }
     }
 
     private onSearchResult(searchResult: SearchResult): void {
+        // Aborted request
+        if (!searchResult) {
+            return;
+        }
+
         this.searchResult = new SearchResult({
             sizeExceeded: searchResult.sizeExceeded,
             searchResults: []
@@ -91,18 +124,33 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
 
         let self = this;
 
-        searchResult.people.forEach(
-            (person: Person) => {
-                this.peopleService
-                    .getPerson(person.userKey)
-                    .then((person: Person) => {
+        this.pendingRequests = searchResult.people.map(
+            (person: IPerson) => {
+                // Store this promise because it is abortable
+                let promise = this.peopleService.getPerson(person.userKey);
+
+                promise
+                    .then((person: IPerson) => {
+                        // Aborted request
+                        if (!person) {
+                            return;
+                        }
+
                         // searchResult may be overwritten by ESC->[LETTER] typed in after a search
                         // has started but before all calls to peopleService.getPerson have resolved
                         if (self.searchResult) {
                             self.searchResult.people.push(person);
                         }
+                    },
+                    (error) => {
+                        self.setErrorMessage(error);
+                    })
+                    .finally(() => {
+                        self.removePendingRequest(promise);
                     });
-            },
-            this);
+
+                return promise;
+            }
+        );
     }
 }

+ 36 - 6
src/main/angular/src/peoplesearch/peoplesearch-table.component.html

@@ -1,29 +1,59 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <mf-app-bar>
-    <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
-    <mf-search-bar search-text="$ctrl.query"
+    <div id="page-content-title" translate="Title_PeopleSearch">People Search</div>
+    <mf-search-bar input-debounce="$ctrl.inputDebounce"
+                   search-text="$ctrl.query"
                    on-search-text-change="$ctrl.onSearchTextChange(value)"
                    on-key-down="$ctrl.onSearchBoxKeyDown($event)"
                    auto-focus></mf-search-bar>
     <span flex></span>
     <mf-icon-button
             icon="view-tile"
+            id="view-title-button"
             ng-click="$ctrl.gotoCardsView()"
             ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
     <mf-icon-button
             icon="view-list"
+            id="view-list-button"
             disabled="true"
             ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
-    <div class="mf-divider vertical"></div>
+    <div class="mf-divider vertical" ng-if="$ctrl.orgChartEnabled"></div>
     <mf-icon-button
             icon="orgchart"
+            id="view-orgchart-button"
             ng-click="$ctrl.gotoOrgchart()"
+            ng-if="$ctrl.orgChartEnabled"
             ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
 </mf-app-bar>
 
 <div class="people-search-component-content">
-    <div class="search-info"
-         ng-if="$ctrl.loading || $ctrl.searchMessage"
-         ng-bind="$ctrl.loading ? ('Display_PleaseWait' | translate) : $ctrl.searchMessage"></div>
+    <div class="search-info-container">
+        <div class="search-info"
+             ng-if="$ctrl.loading || $ctrl.searchMessage || $ctrl.errorMessage"
+             ng-bind="$ctrl.getMessage() || ('Display_PleaseWait' | translate)">
+        </div>
+    </div>
 
     <mf-table data="person in $ctrl.searchResult.people"
               ng-show="$ctrl.searchResult.people.length"

+ 2 - 2
src/main/angular/src/peoplesearch/peoplesearch-table.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 40 - 10
src/main/angular/src/peoplesearch/peoplesearch-table.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,8 +24,11 @@
 import { Component } from '../component';
 import { IConfigService } from '../services/config.service';
 import IPeopleService from '../services/people.service';
-import PeopleSearchBaseComponent from './peoplesearch-base.component';
+import IPwmService from '../services/pwm.service';
 import { IQService, IScope } from 'angular';
+import LocalStorageService from '../services/local-storage.service';
+import PeopleSearchBaseComponent from './peoplesearch-base.component';
+import PromiseService from '../services/promise.service';
 import SearchResult from '../models/search-result.model';
 
 @Component({
@@ -35,15 +38,38 @@ import SearchResult from '../models/search-result.model';
 export default class PeopleSearchTableComponent extends PeopleSearchBaseComponent {
     columnConfiguration: any;
 
-    static $inject = [ '$q', '$scope', '$state', '$stateParams', '$translate', 'ConfigService', 'PeopleService' ];
+    static $inject = [
+        '$q',
+        '$scope',
+        '$state',
+        '$stateParams',
+        '$translate',
+        'ConfigService',
+        'LocalStorageService',
+        'PeopleService',
+        'PromiseService',
+        'PwmService'
+    ];
     constructor($q: IQService,
                 $scope: IScope,
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
-                private configService: IConfigService,
-                peopleService: IPeopleService) {
-        super($q, $scope, $state, $stateParams, $translate, peopleService);
+                configService: IConfigService,
+                localStorageService: LocalStorageService,
+                peopleService: IPeopleService,
+                promiseService: PromiseService,
+                pwmService: IPwmService) {
+        super($q,
+            $scope,
+            $state,
+            $stateParams,
+            $translate,
+            configService,
+            localStorageService,
+            peopleService,
+            promiseService,
+            pwmService);
     }
 
     $onInit(): void {
@@ -53,13 +79,17 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
         let self = this;
 
         // The table columns are dynamic and configured via a service
-        this.configService.getColumnConfiguration().then((columnConfiguration: any) => {
-            self.columnConfiguration = columnConfiguration;
-        });
+        this.configService.getColumnConfig().then(
+            (columnConfiguration: any) => {
+                self.columnConfiguration = columnConfiguration;
+            },
+            (error) => {
+                self.setErrorMessage(error);
+            });
     }
 
     gotoCardsView() {
-        this.gotoState('search.cards');
+        this.toggleView('search.cards');
     }
 
     fetchData() {

+ 7 - 3
src/main/angular/src/peoplesearch/peoplesearch.module.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -30,11 +30,13 @@ import PeopleSearchTableComponent from './peoplesearch-table.component';
 import PeopleSearchCardsComponent from './peoplesearch-cards.component';
 import PersonCardComponent from './person-card.component';
 import PersonDetailsDialogComponent from './person-details-dialog.component';
+import LocalStorageService from '../services/local-storage.service';
+import PromiseService from '../services/promise.service';
 import uxModule from '../ux/ux.module';
 
 require('./peoplesearch.scss');
 
-var moduleName = 'people-search';
+const moduleName = 'people-search';
 
 module(moduleName, [
     'pascalprecht.translate',
@@ -47,6 +49,8 @@ module(moduleName, [
     .component('personCard', PersonCardComponent)
     .component('peopleSearchTable', PeopleSearchTableComponent)
     .component('peopleSearchCards', PeopleSearchCardsComponent)
-    .component('personDetailsDialogComponent', PersonDetailsDialogComponent);
+    .component('personDetailsDialogComponent', PersonDetailsDialogComponent)
+    .service('PromiseService', PromiseService)
+    .service('LocalStorageService', LocalStorageService);
 
 export default moduleName;

+ 30 - 13
src/main/angular/src/peoplesearch/peoplesearch.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -36,9 +36,22 @@ body {
   }
 }
 
+a {
+  color: #0088ce;
+  cursor: pointer;
+  font-weight: normal;
+  text-decoration: none;
+
+  &:focus {
+    outline: 1px solid #28a9e1;
+  }
+
+  &:hover {
+    background-color: #f6f9f8;
+  }
+}
+
 .people-search-component {
-  display: flex;
-  flex-flow: column nowrap;
   height: 100%;
 
   > ui-view {
@@ -50,15 +63,19 @@ body {
     margin-bottom: 10px;
   }
 
-  .search-info {
-    border: 1px solid #dae1e1;
-    border-radius: 3px;
-    color: #808080;
-    display: inline-block;
-    font-size: 14px;
-    margin: 0 auto 10px;
-    padding: 5px;
-    text-align: center;
+  .search-info-container {
+    min-height: 38px;
+
+    .search-info {
+      border: 1px solid #dae1e1;
+      border-radius: 3px;
+      color: #808080;
+      display: inline-block;
+      font-size: 14px;
+      margin: 0 auto 10px;
+      padding: 5px;
+      text-align: center;
+    }
   }
 }
 

+ 24 - 2
src/main/angular/src/peoplesearch/person-card.component.html

@@ -1,5 +1,27 @@
-<div class="person-card-content" ng-switch="$ctrl.size">
-    <div class="avatar" ng-style="$ctrl.getAvatarStyle()" aria-label="User avatar"></div>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<div class="person-card-content" ng-attr-id="{{'displayName-' + $ctrl.person.displayNames[0] }}" ng-switch="$ctrl.size">
+    <div class="avatar" ng-if="$ctrl.isSmall() || $ctrl.showImage" ng-style="$ctrl.getAvatarStyle()" aria-label="User avatar"></div>
     <div class="reports"
          ng-if="$ctrl.showDirectReportCount && $ctrl.person.numDirectReports"
          ng-bind="$ctrl.person.numDirectReports"

+ 26 - 6
src/main/angular/src/peoplesearch/person-card.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -43,7 +43,7 @@ person-card {
   height: $person-card-height;
   padding: $person-card-spacing;
   position: relative;
-  text-align: start;
+  text-align: left;
   vertical-align: top;
   width: $person-card-width;
 
@@ -51,10 +51,18 @@ person-card {
     cursor: pointer;
 
     &:focus,
+    &:hover {
+      outline: none;
+    }
+
+    &:focus {
+      background-color: #fff6ce;
+      border-color: #ffd92d;
+    }
+
     &:hover {
       background-color: $person-card-hover-bg-color;
       border-color: $person-card-border-color;
-      outline: none;
     }
   }
 
@@ -88,7 +96,17 @@ person-card {
       &:focus,
       &:hover {
         background-color: transparent;
+      }
 
+      &:focus {
+        > .person-card-content {
+          > .avatar {
+            border-color: #ffd92d !important;
+          }
+        }
+      }
+
+      &:hover {
         > .person-card-content {
           > .avatar {
             border-color: $person-card-border-color !important;
@@ -125,7 +143,6 @@ person-card {
 
         :first-child {
           font-size: 13px;
-          line-height: 13px;
         }
       }
 
@@ -143,6 +160,7 @@ person-card {
     > .avatar {
       background: transparent url('../../images/user.png') no-repeat center center;
       background-size: contain;
+      border-radius: 3px;
       flex: 0 0 $person-card-avatar-size;
       height: $person-card-avatar-size;
       margin-right: $person-card-spacing;
@@ -176,7 +194,7 @@ person-card {
 
       > .secondary-details {
         border-top: 1px solid #dae1e1;
-        margin-top: 20px;
+        margin-top: 10px;
         padding-top: 8px;
       }
     }
@@ -199,6 +217,8 @@ person-card {
 
 [dir="rtl"] {
   person-card {
+    text-align: right;
+
     &[size="large"] {
       > .person-card-content {
         > .avatar {

+ 22 - 11
src/main/angular/src/peoplesearch/person-card.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,9 +21,9 @@
  */
 
 
-import { IAugmentedJQuery } from 'angular';
+import { isString, IAugmentedJQuery } from 'angular';
 import { Component } from '../component';
-import Person from '../models/person.model';
+import { IPerson } from '../models/person.model';
 import { IPeopleService } from '../services/people.service';
 
 @Component({
@@ -31,6 +31,7 @@ import { IPeopleService } from '../services/people.service';
         directReports: '<',
         disableFocus: '<',
         person: '<',
+        showImage: '<',
         size: '@',
         showDirectReportCount: '<'
     },
@@ -40,10 +41,11 @@ import { IPeopleService } from '../services/people.service';
 export default class PersonCardComponent {
     private details: any[]; // For large style cards
     private disableFocus: boolean;
-    private person: Person;
-    private directReports: Person[];
+    private person: IPerson;
+    private directReports: IPerson[];
     private size: string;
     private showDirectReportCount: boolean;
+    private showImage: boolean;
 
     static $inject = ['$element', 'PeopleService'];
     constructor(private $element: IAugmentedJQuery, private peopleService: IPeopleService) {
@@ -64,12 +66,13 @@ export default class PersonCardComponent {
 
             if (this.showDirectReportCount) {
                 this.peopleService.getNumberOfDirectReports(this.person.userKey)
-                    .then((numDirectReports) => {
-                        this.person.numDirectReports = numDirectReports;
-                    })
-                    .catch(() => {
-                        // TODO: error handling
-                    });
+                    .then(
+                        (numDirectReports) => {
+                            this.person.numDirectReports = numDirectReports;
+                        },
+                        (error) => {
+                            // TODO: handle error. NOOP is fine for now because it won't try to display the result
+                        });
             }
         }
     }
@@ -79,6 +82,10 @@ export default class PersonCardComponent {
     }
 
     getAvatarStyle(): any {
+        if (!this.showImage) {
+            return { 'background-image': 'url()' };
+        }
+
         if (this.person && this.person.photoURL) {
             return { 'background-image': 'url(' + this.person.photoURL + ')' };
         }
@@ -86,6 +93,10 @@ export default class PersonCardComponent {
         return {};
     }
 
+    isSmall(): boolean {
+        return this.size === 'small';
+    }
+
     private onKeyDown(event: KeyboardEvent): void {
         if (event.keyCode === 13 || event.keyCode === 32) { // 13 = Enter, 32 = Space
             this.$element.triggerHandler('click');

+ 77 - 54
src/main/angular/src/peoplesearch/person-details-dialog.component.html

@@ -1,62 +1,85 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <mf-dialog class="person-details" on-close="$ctrl.closeDialog()">
-    <mf-app-bar>
-        <div class="page-content-title" translate="Title_Details">Details</div>
-    </mf-app-bar>
     <div class="mf-dialog-content">
-        <person-card size="medium"
-                     person="$ctrl.person"
-                     disable-focus="true"
-                     show-direct-report-count="false"></person-card>
-        <mf-button type="button" ng-click="$ctrl.gotoOrgChart()">
-            <mf-icon icon="orgchart"></mf-icon>
-            <span translate="Title_OrgChart">Organizational Chart</span>
-        </mf-button>
+        <div class="person-details-header">
+            <person-card size="medium"
+                         person="$ctrl.person"
+                         disable-focus="true"
+                         show-image="$ctrl.photosEnabled"
+                         show-direct-report-count="false"></person-card>
+            <mf-button type="button" ng-click="$ctrl.gotoOrgChart()" ng-if="$ctrl.orgChartEnabled">
+                <mf-icon icon="orgchart" id="orgchart-button"></mf-icon>
+                <span translate="Title_OrgChart">Organizational Chart</span>
+            </mf-button>
+        </div>
 
-        <!-- Details -->
-        <table>
-            <tbody>
-                <tr ng-repeat="reference in $ctrl.person.links">
-                    <td colspan="2">
-                        <div class="detail-link">
-                            <a ng-href="{{reference.link}}"><span ng-bind="reference.name"></span></a>
-                        </div>
-                    </td>
-                </tr>
-                <tr ng-repeat="(key, detail) in $ctrl.person.detail">
-                    <td ng-bind="detail.label"></td>
-                    <td ng-switch="detail.type">
-                        <div class="detail-container" ng-switch-when="userDN">
-                            <ul>
-                                <li ng-repeat="user in detail.userReferences">
-                                    <a ng-href="{{$ctrl.getPersonDetailsUrl(user.userKey)}}"
-                                       ng-bind="user.displayName"></a>
-                                </li>
-                            </ul>
-                        </div>
-                        <div class="detail-container" ng-switch-default>
+        <div class="person-details-content">
+            <table>
+                <tbody>
+                    <tr>
+                        <td></td>
+                        <td>
                             <ul>
-                                <li ng-repeat="value in detail.values">
-                                    <a ng-href="mailto:{{value}}"
-                                       ng-bind="value"
-                                       ng-if="detail.type === 'email'"
-                                       flex></a>
-                                    <a ng-href="tel:{{value}}"
-                                       ng-bind="value"
-                                       ng-if="detail.type === 'tel'"
-                                       flex></a>
-                                    <span ng-bind="value"
-                                          ng-if="detail.type !== 'email' && detail.type !== 'tel'"
-                                          flex></span>
-                                    <mf-icon-button icon="magnify"
-                                                    ng-click="$ctrl.searchText(value)"
-                                                    ng-if="detail.searchable"
-                                                    ng-attr-title="{{'Search \'' + value + '\''}}"></mf-icon-button>
+                                <li ng-repeat="reference in $ctrl.person.links">
+                                    <a ng-href="{{reference.link}}"><span ng-bind="reference.name"></span></a>
                                 </li>
                             </ul>
-                        </div>
-                    </td>
-                </tr>
-            </tbody>
-        </table>
+                        </td>
+                    </tr>
+                    <tr ng-repeat="(key, detail) in $ctrl.person.detail">
+                        <td ng-bind="detail.label"></td>
+                        <td ng-switch="detail.type">
+                            <div class="detail-container" ng-switch-when="userDN">
+                                <ul>
+                                    <li ng-repeat="user in detail.userReferences">
+                                        <a ng-href="{{$ctrl.getPersonDetailsUrl(user.userKey)}}"
+                                           ng-bind="user.displayName"></a>
+                                    </li>
+                                </ul>
+                            </div>
+                            <div class="detail-container" ng-switch-default>
+                                <ul>
+                                    <li ng-repeat="value in detail.values">
+                                        <a ng-href="mailto:{{value}}"
+                                           ng-bind="value"
+                                           ng-if="detail.type === 'email'"></a>
+                                        <a ng-href="tel:{{value}}"
+                                           ng-bind="value"
+                                           ng-if="detail.type === 'tel'"></a>
+                                        <span ng-bind="value"
+                                              ng-if="detail.type !== 'email' && detail.type !== 'tel'"></span>
+                                        <mf-icon-button icon="search_thick"
+                                                        ng-click="$ctrl.searchText(value)"
+                                                        ng-if="detail.searchable"
+                                                        ng-attr-title="{{('Placeholder_Search' | translate) + ' \'' + value + '\''}}"></mf-icon-button>
+                                    </li>
+                                </ul>
+                            </div>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
     </div>
 </mf-dialog>

+ 86 - 53
src/main/angular/src/peoplesearch/person-details-dialog.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,80 +21,113 @@
  */
 
 
-.detail-link {
-  text-align: center;
-  width:100%;
-}
-
 .person-details {
   mf-app-bar {
     padding: 0 5px;
   }
 
   .mf-dialog-content {
-    > table {
-      border: 1px solid #dae1e1;
-      border-collapse: collapse;
-      //box-sizing: border-box;
-      width: 100%;
-
-      tr {
-        height: 25px;
-
-        td {
-          border: 1px solid #dae1e1;
-          font-size: 12px;
-          height: 19px;
-          padding: 3px 5px;
-          text-align: start;
-
-          &:first-child {
-            color: #949494;
-            text-align: end;
-            padding: 3px 15px;
+    > .person-details-header {
+      background-color: #eef2f2;
+
+      > mf-button {
+        margin: 0 0 10px 0;
+      }
+
+      > person-card {
+        height: 120px;
+        max-width: 100%;
+        width: 100%;
+
+        > .person-card-content {
+          > .avatar {
+            flex-basis: 100px;
+            height: 100px;
+            width: 100px;
           }
+        }
+      }
+    }
 
-          &:last-child {
+    > .person-details-content {
+      padding: 10px;
 
-            > .detail-container {
-              a {
-                cursor: pointer;
-                text-decoration: underline;
-              }
+      > table {
+        border: none;
+        border-collapse: collapse;
+        width: 100%;
 
-              > ul {
-                list-style: none;
-                margin: 0;
-                padding: 0;
+        tr {
+          height: 25px;
 
-                > li {
-                  display: flex;
-                  flex-direction: row;
-                  margin: 0;
-                  padding: 0;
+          td {
+            border: none;
+            font-size: 12px;
+            height: 19px;
+            text-align: left;
 
-                  > [flex] {
-                    flex: 1 1;
-                  }
+            &:first-child {
+              color: #949494;
+              width: 100px;
+              text-align: right;
+              padding: 3px 0;
+            }
+
+            &:last-child {
+              padding: 3px 10px;
 
-                  > mf-icon-button {
-                    display: inline-block;
-                    flex: 0 0 16px;
-                    height: 16px;
-                    width: 16px;
+              > .detail-container {
+                > ul {
+                  > li {
+                    > mf-icon-button {
+                      display: inline-block;
+                      height: 16px;
+                      width: 16px;
 
-                    > button {
-                      > mf-icon {
-                        font-size: 16px;
+                      > button {
+                        > mf-icon {
+                          font-size: 16px;
+                        }
                       }
                     }
                   }
                 }
               }
             }
+
+            ul {
+              list-style: none;
+              margin: 0;
+              padding: 0;
+
+              > li {
+                margin: 0;
+                padding: 0;
+              }
+            }
           }
         }
       }
     }
   }
 }
+
+[dir="rtl"] {
+  .person-details {
+    .mf-dialog-content {
+      > .person-details-content {
+        > table {
+          tr {
+            td {
+              text-align: right;
+
+              &:first-child {
+                text-align: left;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 25 - 8
src/main/angular/src/peoplesearch/person-details-dialog.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,33 +22,50 @@
 
 
 import { Component } from '../component';
+import { IConfigService } from '../services/config.service';
 import { IPeopleService } from '../services/people.service';
-import Person from '../models/person.model';
-import { IAugmentedJQuery, IScope, ITimeoutService } from 'angular';
+import { IAugmentedJQuery, ITimeoutService } from 'angular';
+import { IPerson } from '../models/person.model';
 
 @Component({
     stylesheetUrl: require('peoplesearch/person-details-dialog.component.scss'),
     templateUrl: require('peoplesearch/person-details-dialog.component.html')
 })
 export default class PersonDetailsDialogComponent {
-    person: Person;
+    person: IPerson;
+    photosEnabled: boolean;
+    orgChartEnabled: boolean;
+
+    static $inject = [ '$element', '$state', '$stateParams', '$timeout', 'ConfigService', 'PeopleService' ];
 
-    static $inject = [ '$element', '$state', '$stateParams', '$timeout', 'PeopleService' ];
     constructor(private $element: IAugmentedJQuery,
                 private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
                 private $timeout: ITimeoutService,
+                private configService: IConfigService,
                 private peopleService: IPeopleService) {
     }
 
     $onInit(): void {
         const personId = this.$stateParams['personId'];
 
+        this.configService.orgChartEnabled().then((orgChartEnabled: boolean) => {
+            this.orgChartEnabled = orgChartEnabled;
+        });
+
+        this.configService.photosEnabled().then((photosEnabled: boolean) => {
+            this.photosEnabled = photosEnabled;
+        });
+
         this.peopleService
             .getPerson(personId)
-            .then((person: Person) => {
-                this.person = person;
-            });
+            .then(
+                (person: IPerson) => {
+                    this.person = person;
+                },
+                (error) => {
+                    // TODO: Handle error. NOOP for now will not assign person
+                });
     }
 
     $postLink() {

+ 4 - 4
src/main/angular/src/peoplesearch/person.filters.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,10 +21,10 @@
  */
 
 
-import Person from '../models/person.model';
+import { IPerson } from '../models/person.model';
 
-export function FullNameFilter(): (person: Person) => string {
-    return (person: Person): string => {
+export function FullNameFilter(): (person: IPerson) => string {
+    return (person: IPerson): string => {
         return `${person.givenName} ${person.sn}`;
     };
 }

+ 1 - 1
src/main/angular/src/peoplesearch/string.filters.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 37 - 0
src/main/angular/src/route-error-handler.ts

@@ -0,0 +1,37 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IAngularEvent, IRootScopeService } from 'angular';
+import { IStateService } from 'angular-ui-router';
+
+export default [
+    '$transitions',
+    '$state',
+    ($transitions, $state: IStateService) => {
+        $transitions.onError({}, (transition) => {
+            if (transition._error === 'OrgChart disabled') {
+                $state.go('search.cards');
+            }
+        });
+    }
+];

+ 46 - 10
src/main/angular/src/routes.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,20 +21,32 @@
  */
 
 
+import { IConfigService } from './services/config.service';
+import { IQService } from 'angular';
+import LocalStorageService from './services/local-storage.service';
+
 export default [
     '$stateProvider',
     '$urlRouterProvider',
-    '$locationProvider',
     (
         $stateProvider: angular.ui.IStateProvider,
-        $urlRouterProvider: angular.ui.IUrlRouterProvider,
-        $locationProvider: angular.ILocationProvider
+        $urlRouterProvider: angular.ui.IUrlRouterProvider
     ) => {
-        $urlRouterProvider.otherwise('/search/cards');
-        $locationProvider.html5Mode({
-            enabled: true,
-            requireBase: false
-        });
+        $urlRouterProvider.otherwise(
+            ($injector: angular.auto.IInjectorService, $location: angular.ILocationService) => {
+                let $state: angular.ui.IStateService = <angular.ui.IStateService>$injector.get('$state');
+                let localStorageService: LocalStorageService =
+                    <LocalStorageService>$injector.get('LocalStorageService');
+
+                let storedView = localStorageService.getItem(localStorageService.keys.SEARCH_VIEW);
+
+                if (storedView) {
+                    $state.go(storedView);
+                }
+                else {
+                    $location.url('search/cards');
+                }
+            });
 
         $stateProvider.state('search', {
             url: '/search?query',
@@ -51,7 +63,31 @@ export default [
             url: '/details/{personId}',
             component: 'personDetailsDialogComponent'
         });
-        $stateProvider.state('orgchart', { url: '/orgchart?query', abstract: true, template: '<ui-view/>' });
+        $stateProvider.state('orgchart', { url: '/orgchart?query',
+            abstract: true,
+            template: '<ui-view/>',
+            resolve: {
+                enabled: [
+                    '$q',
+                    'ConfigService',
+                    ($q: IQService, configService: IConfigService) => {
+                        let deferred = $q.defer();
+
+                        configService
+                            .orgChartEnabled()
+                            .then((orgChartEnabled: boolean) => {
+                                if (!orgChartEnabled) {
+                                    deferred.reject('OrgChart disabled');
+                                }
+                                else {
+                                    deferred.resolve();
+                                }
+                            });
+
+                        return deferred.promise;
+                    }]
+            }
+        });
         $stateProvider.state('orgchart.index', { url: '', component: 'orgChartSearch' });
         $stateProvider.state('orgchart.search', { url: '/{personId}', component: 'orgChartSearch' });
         $stateProvider.state('orgchart.search.details', { url: '/details', component: 'personDetailsDialogComponent' });

+ 14 - 2
src/main/angular/src/services/config.service.dev.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@ export default class ConfigService implements IConfigService {
     static $inject = [ '$q' ];
     constructor(private $q: IQService) {}
 
-    getColumnConfiguration(): IPromise<any> {
+    getColumnConfig(): IPromise<any> {
         return this.$q.resolve({
             givenName: 'First Name',
             sn: 'Last Name',
@@ -37,4 +37,16 @@ export default class ConfigService implements IConfigService {
             telephoneNumber: 'Telephone'
         });
     }
+
+    photosEnabled(): IPromise<boolean> {
+        return this.$q.resolve(true);
+    }
+
+    orgChartEnabled(): IPromise<boolean> {
+        return this.$q.resolve(true);
+    };
+
+    getValue(key: string): IPromise<any> {
+        return null;
+    }
 }

+ 47 - 8
src/main/angular/src/services/config.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,25 +21,64 @@
  */
 
 
-import { IHttpService, IPromise, IQService } from 'angular';
+import { IHttpService, ILogService, IPromise, IQService } from 'angular';
+import IPwmService from './pwm.service';
 import PwmService from './pwm.service';
 
+const COLUMN_CONFIG = 'peoplesearch_search_columns';
+const PHOTO_ENABLED = 'peoplesearch_enablePhoto';
+const ORGCHART_ENABLED = 'peoplesearch_orgChartEnabled';
+
 export interface IConfigService {
-    getColumnConfiguration(): IPromise<any>;
+    getColumnConfig(): IPromise<any>;
+    photosEnabled(): IPromise<boolean>;
+    orgChartEnabled(): IPromise<boolean>;
+    getValue(key: string): IPromise<any>;
 }
 
 export default class ConfigService implements IConfigService {
-    static $inject = ['$http', '$q', 'PwmService' ];
+
+    static $inject = ['$http', '$log', '$q', 'PwmService' ];
     constructor(private $http: IHttpService,
+                private $log: ILogService,
                 private $q: IQService,
-                private pwmService: PwmService) {
+                private pwmService: IPwmService) {
+    }
+
+    getColumnConfig(): IPromise<any> {
+        return this.getValue(COLUMN_CONFIG);
+    }
+
+    photosEnabled(): IPromise<boolean> {
+        return this.getValue(PHOTO_ENABLED)
+            .then(null, () => { return this.$q.resolve(true); }); // On error use default
+    }
+
+    orgChartEnabled(): IPromise<boolean> {
+        return this.getValue(ORGCHART_ENABLED)
+            .then(null, () => { return this.$q.resolve(true); }); // On error use default
     }
 
-    getColumnConfiguration(): IPromise<any> {
+    getValue(key: string): IPromise<any> {
         return this.$http
             .get(this.pwmService.getServerUrl('clientData'), { cache: true })
             .then((response) => {
-                return this.$q.resolve(response.data['data']['peoplesearch_search_columns']);
-            });
+                if (response.data['error']) {
+                    return this.handlePwmError(response);
+                }
+
+                return this.$q.resolve(response.data['data'][key]);
+            }, this.handleHttpError);
+    }
+
+    private handleHttpError(error): void {
+        this.$log.error(error);
+    }
+
+    private handlePwmError(response): IPromise<any> {
+        const errorMessage = `${response.data['errorCode']}: ${response.data['errorMessage']}`;
+        this.$log.error(errorMessage);
+
+        return this.$q.reject(response.data['errorMessage']);
     }
 }

+ 67 - 0
src/main/angular/src/services/local-storage.service.ts

@@ -0,0 +1,67 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { ILogService, IWindowService } from 'angular';
+
+const PWM_PREFIX = 'PWM_';
+const KEYS = {
+    SEARCH_TEXT: 'searchText',
+    SEARCH_VIEW: 'searchView'
+};
+
+export default class LocalStorageService {
+    keys: any = KEYS;
+    private localStorageEnabled: boolean = true;
+
+    static $inject = [ '$log', '$window' ];
+    constructor($log: ILogService, private $window: IWindowService) {
+        if (!$window.sessionStorage.getItem) {
+            this.localStorageEnabled = false;
+            $log.info('Local Storage API not enabled. Using NOOP implementation.');
+        }
+    }
+
+    getItem(key: string): any {
+        if (this.localStorageEnabled) {
+            return this.$window.sessionStorage[this.prepKey(key)];
+        }
+
+        return null;
+    }
+
+    setItem(key: string, value: any): void {
+        if (this.localStorageEnabled) {
+            this.$window.sessionStorage[this.prepKey(key)] = value;
+        }
+    }
+
+    removeItem(key: string): any {
+        if (this.localStorageEnabled) {
+            return this.$window.sessionStorage.removeItem(this.prepKey(key));
+        }
+    }
+
+    private prepKey(key: string) {
+        return PWM_PREFIX + key;
+    }
+}

+ 147 - 21
src/main/angular/src/services/people.data.json

@@ -71,7 +71,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/1",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 2,
@@ -145,7 +151,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/2",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 3,
@@ -219,7 +231,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/3",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 4,
@@ -293,7 +311,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/4",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 5,
@@ -367,7 +391,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/5",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 6,
@@ -441,7 +471,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/6",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 7,
@@ -515,7 +551,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/7",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 8,
@@ -589,7 +631,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/8",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 9,
@@ -663,7 +711,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/9",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 10,
@@ -737,7 +791,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/10",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 11,
@@ -811,7 +871,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/11",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 12,
@@ -885,7 +951,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/12",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 13,
@@ -959,7 +1031,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/13",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 14,
@@ -1033,7 +1111,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/14",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 15,
@@ -1107,7 +1191,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/15",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 16,
@@ -1181,7 +1271,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/16",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 17,
@@ -1255,7 +1351,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/17",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 18,
@@ -1329,7 +1431,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/18",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 19,
@@ -1403,7 +1511,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/19",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 20,
@@ -1477,7 +1591,13 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/20",
+        "name": "Organization"
+      }
+    ]
   },
   {
     "userKey": 21,
@@ -1551,6 +1671,12 @@
           }
         ]
       }
-    }
+    },
+    "links": [
+      {
+        "link": "/orgchart/21",
+        "name": "Organization"
+      }
+    ]
   }
 ]

+ 70 - 39
src/main/angular/src/services/people.service.dev.ts

@@ -3,12 +3,12 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
+ * (at your option) any later versionI.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -21,24 +21,26 @@
  */
 
 
-import { IPromise, IQService } from 'angular';
-import Person from '../models/person.model';
+import { IPromise, IQService, ITimeoutService } from 'angular';
+import { IPerson } from '../models/person.model';
 import { IPeopleService } from './people.service';
 import OrgChartData from '../models/orgchart-data.model';
 import SearchResult from '../models/search-result.model';
 
 const peopleData = require('./people.data');
+
 const MAX_RESULTS = 10;
+const SIMULATED_RESPONSE_TIME = 0;
 
 export default class PeopleService implements IPeopleService {
-    private people: Person[];
+    private people: IPerson[];
 
-    static $inject = ['$q'];
-    constructor(private $q: IQService) {
-        this.people = peopleData.map((person) => new Person(person));
+    static $inject = ['$q', '$timeout' ];
+    constructor(private $q: IQService, private $timeout: ITimeoutService) {
+        this.people = peopleData.map((person) => <IPerson>(person));
 
         // Create directReports detail (instead of managing this in people.data.json
-        this.people.forEach((person: Person) => {
+        this.people.forEach((person: IPerson) => {
             const directReports = this.findDirectReports(person.userKey);
 
             if (!directReports.length) {
@@ -50,7 +52,7 @@ export default class PeopleService implements IPeopleService {
                 label: 'Direct Reports',
                 type: 'userDN',
                 userReferences: directReports
-                    .map((directReport: Person) => {
+                    .map((directReport: IPerson) => {
                         return {
                             userKey: directReport.userKey,
                             displayName: directReport._displayName
@@ -60,7 +62,7 @@ export default class PeopleService implements IPeopleService {
         }, this);
     }
 
-    autoComplete(query: string): IPromise<Person[]> {
+    autoComplete(query: string): IPromise<IPerson[]> {
         return this.search(query)
             .then((searchResult: SearchResult) => {
                 let people = searchResult.people;
@@ -75,17 +77,17 @@ export default class PeopleService implements IPeopleService {
             });
     }
 
-    getDirectReports(id: string): angular.IPromise<Person[]> {
+    getDirectReports(id: string): angular.IPromise<IPerson[]> {
         const people = this.findDirectReports(id);
 
         return this.$q.resolve(people);
     }
 
-    getManagementChain(id: string): angular.IPromise<Person[]> {
+    getManagementChain(id: string): angular.IPromise<IPerson[]> {
         let person = this.findPerson(id);
 
         if (person) {
-            const managementChain: Person[] = [];
+            const managementChain: IPerson[] = [];
 
             while (person = this.findManager(person)) {
                 managementChain.push(person);
@@ -113,51 +115,80 @@ export default class PeopleService implements IPeopleService {
 
     getNumberOfDirectReports(personId: string): IPromise<number> {
         return this.getDirectReports(personId)
-            .then((directReports: Person[]) => {
+            .then((directReports: IPerson[]) => {
                 return this.$q.resolve(directReports.length);
             });
     }
 
-    getPerson(id: string): IPromise<Person> {
-        const person = this.findPerson(id);
+    getPerson(id: string): IPromise<IPerson> {
+        let self = this;
 
-        if (person) {
-            return this.$q.resolve(person);
-        }
+        let deferred = this.$q.defer();
+        let deferredAbort = this.$q.defer();
 
-        return this.$q.reject(`Person with id: "${id}" not found.`);
-    }
+        let timeoutPromise = this.$timeout(() => {
+            const person = this.findPerson(id);
+
+            if (person) {
+                deferred.resolve(person);
+            }
+            else {
+                deferred.reject(`Person with id: "${id}" not found.`);
+            }
+        }, SIMULATED_RESPONSE_TIME);
+
+        // To simulate an abortable promise, edit SIMULATED_RESPONSE_TIME
+        deferred.promise['_httpTimeout'] = deferredAbort;
+        deferredAbort.promise.then(() => {
+            self.$timeout.cancel(timeoutPromise);
+            deferred.resolve();
+        });
 
-    isOrgChartEnabled(id: string): angular.IPromise<boolean> {
-        return this.$q.resolve(true);
+        return deferred.promise;
     }
 
     search(query: string): angular.IPromise<SearchResult> {
-        let people = this.people.filter((person: Person) => {
-            if (!query) {
-                return false;
+        let self = this;
+
+        let deferred = this.$q.defer();
+        let deferredAbort = this.$q.defer();
+
+        let timeoutPromise = this.$timeout(() => {
+            let people = this.people.filter((person: IPerson) => {
+                if (!query) {
+                    return false;
+                }
+                return person._displayName.toLowerCase().indexOf(query.toLowerCase()) >= 0;
+            });
+
+            const sizeExceeded = (people.length > MAX_RESULTS);
+            if (sizeExceeded) {
+                people = people.slice(MAX_RESULTS);
             }
-            return person._displayName.toLowerCase().indexOf(query.toLowerCase()) >= 0;
-        });
 
-        const sizeExceeded = (people.length > MAX_RESULTS);
-        if (sizeExceeded) {
-            people = people.slice(MAX_RESULTS);
-        }
+            deferred.resolve(new SearchResult({sizeExceeded: sizeExceeded, searchResults: people}));
+        }, SIMULATED_RESPONSE_TIME * 6);
+
+        // To simulate an abortable promise, edit SIMULATED_RESPONSE_TIME
+        deferred.promise['_httpTimeout'] = deferredAbort;
+        deferredAbort.promise.then(() => {
+            self.$timeout.cancel(timeoutPromise);
+            deferred.resolve();
+        });
 
-        return this.$q.resolve(new SearchResult({sizeExceeded: sizeExceeded, searchResults: people}));
+        return deferred.promise;
     }
 
-    private findDirectReports(id: string): Person[] {
-        return this.people.filter((person: Person) => person.detail['manager']['userReferences'][0].userKey == id);
+    private findDirectReports(id: string): IPerson[] {
+        return this.people.filter((person: IPerson) => person.detail['manager']['userReferences'][0].userKey == id);
     }
 
-    private findManager(person: Person): Person {
+    private findManager(person: IPerson): IPerson {
         return this.findPerson(person.detail['manager']['userReferences'][0].userKey);
     }
 
-    private findPerson(id: string): Person {
-        const people = this.people.filter((person: Person) => person.userKey == id);
+    private findPerson(id: string): IPerson {
+        const people = this.people.filter((person: IPerson) => person.userKey == id);
 
         if (people.length) {
             return people[0];

+ 125 - 57
src/main/angular/src/services/people.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,28 +21,41 @@
  */
 
 
-import { IHttpService, IPromise, IQService } from 'angular';
-import Person from '../models/person.model';
-import PwmService from './pwm.service';
+import { isString, IHttpService, ILogService, IPromise, IQService, IWindowService } from 'angular';
+import { IPerson } from '../models/person.model';
+import IPwmService from './pwm.service';
 import OrgChartData from '../models/orgchart-data.model';
 import SearchResult from '../models/search-result.model';
 
 export interface IPeopleService {
-    autoComplete(query: string): IPromise<Person[]>;
-    getDirectReports(personId: string): IPromise<Person[]>;
+    autoComplete(query: string): IPromise<IPerson[]>;
+    getDirectReports(personId: string): IPromise<IPerson[]>;
     getNumberOfDirectReports(personId: string): IPromise<number>;
-    getManagementChain(personId: string): IPromise<Person[]>;
-    getOrgChartData(personId: string): IPromise<OrgChartData>;
-    getPerson(id: string): IPromise<Person>;
-    isOrgChartEnabled(id: string): IPromise<boolean>;
+    getManagementChain(personId: string): IPromise<IPerson[]>;
+    getOrgChartData(personId: string, skipChildren: boolean): IPromise<OrgChartData>;
+    getPerson(id: string): IPromise<IPerson>;
     search(query: string): IPromise<SearchResult>;
 }
 
 export default class PeopleService implements IPeopleService {
-    static $inject = ['$http', '$q', 'PwmService' ];
-    constructor(private $http: IHttpService, private $q: IQService, private pwmService: PwmService) {}
+    PWM_GLOBAL: any;
+
+    static $inject = ['$http', '$log', '$q', 'PwmService', '$window' ];
+    constructor(private $http: IHttpService,
+                private $log: ILogService,
+                private $q: IQService,
+                private pwmService: IPwmService,
+                $window: IWindowService)
+    {
+        if ($window['PWM_GLOBAL']) {
+            this.PWM_GLOBAL = $window['PWM_GLOBAL'];
+        }
+        else {
+            this.$log.warn('PWM_GLOBAL is not defined on window');
+        }
+    }
 
-    autoComplete(query: string): IPromise<Person[]> {
+    autoComplete(query: string): IPromise<IPerson[]> {
         return this.search(query, { 'includeDisplayName': true })
             .then((searchResult: SearchResult) => {
                 let people = searchResult.people;
@@ -55,12 +68,12 @@ export default class PeopleService implements IPeopleService {
             });
     }
 
-    getDirectReports(id: string): IPromise<Person[]> {
-        return this.getOrgChartData(id).then((orgChartData: OrgChartData) => {
-            let people: Person[] = [];
+    getDirectReports(id: string): IPromise<IPerson[]> {
+        return this.getOrgChartData(id, false).then((orgChartData: OrgChartData) => {
+            let people: IPerson[] = [];
 
             for (let directReport of orgChartData.children) {
-                let person: Person = new Person(directReport);
+                let person: IPerson = <IPerson>(directReport);
                 people.push(person);
             }
 
@@ -68,69 +81,124 @@ export default class PeopleService implements IPeopleService {
         });
     }
 
-    getNumberOfDirectReports(personId: string): IPromise<number> {
-        return this.getOrgChartData(personId).then((orgChartData: OrgChartData) => {
-            return this.$q.resolve(orgChartData.children.length);
+    getNumberOfDirectReports(id: string): IPromise<number> {
+        return this.getDirectReports(id).then((people: IPerson[]) => {
+            return this.$q.resolve(people.length);
         });
     }
 
-    getManagementChain(id: string): IPromise<Person[]> {
-        let people: Person[] = [];
+    getManagementChain(id: string): IPromise<IPerson[]> {
+        let people: IPerson[] = [];
         return this.getManagerRecursive(id, people);
     }
 
-    private getManagerRecursive(id: string, people: Person[]): IPromise<Person[]> {
-        return this.getOrgChartData(id).then((orgChartData: OrgChartData) => {
-            if (orgChartData.manager) {
-                people.push(orgChartData.manager);
+    private getManagerRecursive(id: string, people: IPerson[]): IPromise<IPerson[]> {
+        return this.getOrgChartData(id, true)
+            .then((orgChartData: OrgChartData) => {
+                if (orgChartData.manager) {
+                    people.push(orgChartData.manager);
 
-                return this.getManagerRecursive(orgChartData.manager.userKey, people);
-            }
+                    return this.getManagerRecursive(orgChartData.manager.userKey, people);
+                }
 
-            return this.$q.resolve(people);
-        });
+                return this.$q.resolve(people);
+            });
     }
 
-    getOrgChartData(personId: string): angular.IPromise<OrgChartData> {
+    getOrgChartData(personId: string, noChildren: boolean): angular.IPromise<OrgChartData> {
         return this.$http
-            .get(this.pwmService.getServerUrl('orgChartData'), { cache: true, params: { userKey: personId } })
-            .then((response) => {
-                let responseData = response.data['data'];
+            .get(this.pwmService.getServerUrl('orgChartData'), {
+                cache: true,
+                params: {
+                    userKey: personId,
+                    noChildren: noChildren
+                }
+            })
+            .then(
+                (response) => {
+                    if (response.data['error']) {
+                        return this.handlePwmError(response);
+                    }
+
+                    let responseData = response.data['data'];
+
+                    let manager: IPerson;
+                    if ('parent' in responseData) { manager = <IPerson>(responseData['parent']); }
+                    const children = responseData['children'].map((child: any) => <IPerson>(child));
+                    const self = <IPerson>(responseData['self']);
+
+                    return this.$q.resolve(new OrgChartData(manager, children, self));
+                },
+                this.handleHttpError.bind(this));
+    }
 
-                let manager: Person;
-                if ('parent' in responseData) { manager = new Person(responseData['parent']); }
-                const children = responseData['children'].map((child: any) => new Person(child));
-                const self = new Person(responseData['self']);
+    getPerson(id: string): IPromise<IPerson> {
+        // Deferred object used for aborting requests. See promise.service.ts for more information
+        let httpTimeout = this.$q.defer();
 
-                return this.$q.resolve(new OrgChartData(manager, children, self));
+        let request = this.$http
+            .get(this.pwmService.getServerUrl('detail'), {
+                cache: true,
+                params: { userKey: id },
+                timeout: httpTimeout.promise
             });
-    }
 
-    getPerson(id: string): IPromise<Person> {
-        return this.$http
-            .get(this.pwmService.getServerUrl('detail'), { cache: true, params: { userKey: id } })
-            .then((response) => {
-                let person: Person = new Person(response.data['data']);
+        let promise = request.then(
+            (response) => {
+                if (response.data['error']) {
+                    return this.handlePwmError(response);
+                }
+
+                let person: IPerson = <IPerson>(response.data['data']);
                 return this.$q.resolve(person);
-            });
-    }
+            },
+            this.handleHttpError.bind(this));
 
-    isOrgChartEnabled(id: string): IPromise<boolean> {
-        // TODO: need to read this from the server
-        return this.$q.resolve(true);
+        promise['_httpTimeout'] = httpTimeout;
+
+        return promise;
     }
 
     search(query: string, params?: any): IPromise<SearchResult> {
-        return this.$http
-            .get(
-                this.pwmService.getServerUrl('search', params),
-                { cache: true, params: { username: query } }
-            )
-            .then((response) => {
+        // Deferred object used for aborting requests. See promise.service.ts for more information
+        let httpTimeout = this.$q.defer();
+        let formID = encodeURIComponent('&pwmFormID=' + this.PWM_GLOBAL['pwmFormID']);
+        // Search window references to PWM_GLOBAL and PWM_MAIN add by legacy PWM code
+        let request = this.$http
+            .post(this.pwmService.getServerUrl('search') + '&pwmFormID=' + this.PWM_GLOBAL['pwmFormID'], {
+                timeout: httpTimeout.promise,
+                username: query,
+                pwmFormID: formID
+            }, {
+                headers: {'Content-Type': 'multipart/form-data'},
+            });
+
+        let promise = request.then(
+            (response) => {
+                if (response.data['error']) {
+                    return this.handlePwmError(response);
+                }
+
                 let receivedData: any = response.data['data'];
                 let searchResult: SearchResult = new SearchResult(receivedData);
 
                 return this.$q.resolve(searchResult);
-            });
+            },
+            this.handleHttpError.bind(this));
+
+        promise['_httpTimeout'] = httpTimeout;
+
+        return promise;
+    }
+
+    private handleHttpError(error): void {
+        this.$log.error(error);
+    }
+
+    private handlePwmError(response): IPromise<any> {
+        const errorMessage = `${response.data['errorCode']}: ${response.data['errorMessage']}`;
+        this.$log.error(errorMessage);
+
+        return this.$q.reject(response.data['errorMessage']);
     }
 }

+ 9 - 13
src/main/java/password/pwm/util/BCrypt.java → src/main/angular/src/services/promise.service.ts

@@ -20,21 +20,17 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
 
-import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
+import { IPromise, IQService } from 'angular';
 
-import java.security.SecureRandom;
+// Pattern explained at https://www.bennadel.com/blog/2731-canceling-a-promise-in-angularjs.htm
+export default class PromiseService {
+    static $inject = [ '$q' ];
+    constructor(private $q: IQService) {}
 
-public class BCrypt {
-    public static String hashPassword(final String password) {
-        final int bcryptRounds = 10;
-        final byte[] salt = new byte[16];
-        (new SecureRandom()).nextBytes(salt);
-        return OpenBSDBCrypt.generate(password.toLowerCase().toCharArray(), salt, bcryptRounds);
-    }
-
-    public static boolean testAnswer(final String password, final String hashedPassword) {
-        return OpenBSDBCrypt.checkPassword(hashedPassword, password.toLowerCase().toCharArray());
+    abort(promise: IPromise<any>) {
+        if (promise && promise['_httpTimeout'] && promise['_httpTimeout'].resolve) {
+            promise['_httpTimeout'].resolve();
+        }
     }
 }

+ 42 - 0
src/main/angular/src/services/pwm.service.dev.ts

@@ -0,0 +1,42 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IPwmService } from './pwm.service';
+
+export default class PwmService implements IPwmService {
+    getServerUrl(processAction: string, additionalParameters?: any): string {
+        return null;
+    }
+
+    get ajaxTypingWait(): number {
+        return 300;
+    }
+
+    get localeStrings(): any {
+        return {};
+    }
+
+    get startupFunctions(): any[] {
+        return [];
+    }
+}

+ 20 - 12
src/main/angular/src/services/pwm.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,16 @@
 
 import { ILogService, IWindowService } from 'angular';
 
-export default class PwmService {
+export interface IPwmService {
+    getServerUrl(processAction: string, additionalParameters?: any): string;
+    ajaxTypingWait: number;
+    localeStrings: any;
+    startupFunctions: any[];
+}
+
+const DEFAULT_AJAX_TYPING_WAIT = 700;
+
+export default class PwmService implements IPwmService {
     PWM_GLOBAL: any;
     PWM_MAIN: any;
 
@@ -51,13 +60,20 @@ export default class PwmService {
     }
 
     getServerUrl(processAction: string, additionalParameters?: any): string {
-        let url: string = this.urlContext + '/private/peoplesearch?processAction=' + processAction;
+        let url: string = window.location.pathname + '?processAction=' + processAction;
         url = this.addParameters(url, additionalParameters);
-        url = this.addPwmFormIdToUrl(url);
 
         return url;
     }
 
+    get ajaxTypingWait(): number {
+        if (this.PWM_GLOBAL) {
+            return this.PWM_GLOBAL['client.ajaxTypingWait'] || DEFAULT_AJAX_TYPING_WAIT;
+        }
+
+        return DEFAULT_AJAX_TYPING_WAIT;
+    }
+
     get localeStrings(): any {
         if (this.PWM_GLOBAL) {
             return this.PWM_GLOBAL['localeStrings'];
@@ -74,14 +90,6 @@ export default class PwmService {
         return [];
     }
 
-    private addPwmFormIdToUrl(url: string): string {
-        if (!this.PWM_MAIN) {
-            return url;
-        }
-
-        return this.PWM_MAIN.addPwmFormIDtoURL(url);
-    }
-
 
     private addParameters(url: string, params: any): string {
         if (!this.PWM_MAIN) {

+ 4 - 11
src/main/angular/src/services/translations-loader.factory.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -23,20 +23,13 @@
 
 import 'angular-translate';
 import { IQService } from 'angular';
-import PwmService from './pwm.service';
+import IPwmService from './pwm.service';
 
 export default [
     '$q',
     'PwmService',
-    ($q: IQService, pwmService: PwmService) => {
+    ($q: IQService, pwmService: IPwmService) => {
         return function () {
-            var deferred = $q.defer();
-
-            pwmService.startupFunctions.push(() => {
-                deferred.resolve(pwmService.localeStrings['Display']);
-            });
-
-            // resolve with translation data
-            return deferred.promise;
+            return $q.resolve(pwmService.localeStrings['Display']);
         };
     }];

+ 5 - 3
src/main/angular/src/ux/app-bar.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -56,10 +56,12 @@ mf-app-bar {
     }
 
     > span[flex] {
+      display: inline-block;
       flex: 1 1;
     }
 
-    > .page-content-title {
+    > #page-content-title {
+      font-size: 24px;
       height: $mf-app-bar-height;
       line-height: $mf-app-bar-height;
     }

+ 1 - 1
src/main/angular/src/ux/app-bar.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 13 - 3
src/main/angular/src/ux/auto-complete.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -55,7 +55,7 @@ mf-auto-complete {
       overflow: hidden;
       padding: 0 10px;
       text-overflow: ellipsis;
-      text-align: start;
+      text-align: left;
       white-space: nowrap;
       width: 100%;
 
@@ -73,4 +73,14 @@ mf-auto-complete {
       }
     }
   }
+}
+
+[dir="rtl"] {
+  mf-auto-complete {
+    > .results {
+      > li {
+        text-align: right;
+      }
+    }
+  }
 }

+ 13 - 5
src/main/angular/src/ux/auto-complete.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } fro
 @Component({
     bindings: {
         'onSearchTextChange': '&',
+        'inputDebounce': '<',
         'itemSelected': '&',
         'item': '@',
         'itemText': '@',
@@ -42,7 +43,8 @@ import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } fro
             contentTemplate.detach();
 
             return `
-                <mf-search-bar search-text="$ctrl.searchText"
+                <mf-search-bar input-debounce="$ctrl.inputDebounce"
+                               search-text="$ctrl.searchText"
                                on-search-text-change="$ctrl.onSearchBarTextChange(value)"                           
                                on-key-down="$ctrl.onSearchBarKeyDown($event)"
                                ng-click="$ctrl.onSearchBarClick($event)"
@@ -53,7 +55,8 @@ import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } fro
                        ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">` +
                 contentTemplate.html().replace(new RegExp($attrs['item'], 'g'), 'item') +
                     `</li>
-                    <li class="search-message" ng-if="$ctrl.show && $ctrl.searchText && !$ctrl.items.length">
+                    <li class="search-message" 
+                        ng-if="$ctrl.show && $ctrl.searchText && !$ctrl.loading && !$ctrl.items.length">
                         <span translate="Display_SearchResultsNone"></span>
                     </li>
                 </ul>`;
@@ -64,6 +67,7 @@ export default class AutoCompleteComponent {
     item: string;
     items: any[];
     itemSelected: (item: any) => void;
+    loading: boolean;
     onSearchTextChange: Function;
     searchText: string;
     searchFunction: (query: any) => IPromise<any[]>;
@@ -92,7 +96,7 @@ export default class AutoCompleteComponent {
     }
 
     $postLink(): void {
-        var self = this;
+        let self = this;
 
         // Listen for clicks outside of the auto-complete component
         // Implemented as a click event instead of a blur event, so the results list can be clicked
@@ -186,11 +190,15 @@ export default class AutoCompleteComponent {
     }
 
     private fetchAutoCompleteData(value: string): void {
-        var self = this;
+        this.loading = true;
+        const self = this;
         this.searchFunction({ query: value })
             .then((results: any[]) => {
                 self.items = results;
                 self.resetSelection();
+            })
+            .finally(() => {
+                self.loading = false;
             });
     }
 

+ 10 - 3
src/main/angular/src/ux/button.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -39,9 +39,16 @@ mf-button {
 
     &:focus,
     &:hover {
-      border-color: #28a9e1;
       color: #0088ce;
       outline: none;
     }
+
+    &:focus {
+      border-color: #28a9e1;
+    }
+
+    &:hover {
+      border-color: #dae1e1;
+    }
   }
 }

+ 1 - 1
src/main/angular/src/ux/button.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 23 - 1
src/main/angular/src/ux/dialog.component.html

@@ -1,6 +1,28 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
 <div class="scrim" ng-click="$ctrl.closeDialog()"></div>
 <div class="mf-dialog-container">
-    <mf-icon-button icon="close" ng-click="$ctrl.closeDialog()"></mf-icon-button>
+    <mf-icon-button icon="close_thick" ng-click="$ctrl.closeDialog()"></mf-icon-button>
 
     <ng-transclude></ng-transclude>
 </div>

+ 5 - 17
src/main/angular/src/ux/dialog.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -49,15 +49,11 @@ mf-dialog {
       position: absolute;
       right: 1px;
       top: 1px;
-    }
-
-    mf-app-bar {
-      background-color: #eaeaea;
+      z-index: 1;
     }
 
     .mf-dialog-content {
       text-align: center;
-      padding: 10px;
 
       > person-card {
         max-width: 100%;
@@ -70,25 +66,17 @@ mf-dialog {
 @media (min-width: 420px) {
   mf-dialog {
     > .mf-dialog-container {
-      border: 2px solid #dddddd;
+      border: 1px solid #dddddd;
       border-radius: 3px;
       height: auto;
       left: 50%;
-      max-width: 100vw;
+      max-width: 420px;
       top: 50%;
       transform: translate(-50%, -50%);
     }
   }
 }
 
-@media (min-width: 514px) {
-  mf-dialog {
-    > .mf-dialog-container {
-      max-width: 514px;
-    }
-  }
-}
-
 [dir="rtl"] {
   mf-dialog {
     > .mf-dialog-container {

+ 1 - 1
src/main/angular/src/ux/dialog.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/ux/dialog.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/ux/element-size.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 10 - 3
src/main/angular/src/ux/icon-button.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -47,10 +47,17 @@ mf-icon-button {
       &:focus,
       &:hover {
         background-color: $mf-icon-button-hover-bg-color;
-        border-color: #dae1e1;
         color: $mf-icon-button-hover-color;
         outline: none;
       }
+
+      &:focus {
+        border-color: #28a9e1;
+      }
+
+      &:hover {
+        border-color: #dae1e1;
+      }
     }
   }
 

+ 1 - 1
src/main/angular/src/ux/icon-button.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 2 - 2
src/main/angular/src/ux/icon.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/ux/icon.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 27 - 5
src/main/angular/src/ux/search-bar.component.html

@@ -1,12 +1,34 @@
-<mf-icon class="search-icon" icon="magnify"></mf-icon>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<mf-icon class="search-icon" icon="search_thick"></mf-icon>
 <input type="text"
+       id="search-bar"
        ng-blur="$ctrl.onBlur({ $event: $event })"
        ng-focus="$ctrl.onFocus({ $event: $event })"
-       ng-keydown="$ctrl.onKeyDown({ $event: $event })"
+       ng-keydown="$ctrl.onInputKeyDown($event)"
        ng-model="$ctrl.searchText"
-       ng-model-options="{ debounce: 300 }"
+       ng-model-options="{ debounce: $ctrl.inputDebounce }"
        ng-attr-placeholder="{{ ('Placeholder_Search' | translate) }}"
-       title="Search Box"
        autocomplete="off" />
-<mf-icon-button class="clear-input" icon="close" ng-click="$ctrl.clearSearchText()"></mf-icon-button>
+<mf-icon-button class="clear-input" icon="close_thick" ng-click="$ctrl.clearSearchText()"></mf-icon-button>
 <!-- loader graphic -->

+ 6 - 2
src/main/angular/src/ux/search-bar.component.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -38,6 +38,10 @@ mf-search-bar {
     line-height: 100%;
     padding: 0 20px;
     width: 100%;
+
+    &[type=text]::-ms-clear {
+      display: none;
+    }
   }
 
   > .clear-input {

+ 15 - 4
src/main/angular/src/ux/search-bar.component.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -28,6 +28,7 @@ import { IAugmentedJQuery, ICompileService, IScope } from 'angular';
 @Component({
     bindings: {
         autoFocus: '@',
+        inputDebounce: '<',
         onSearchTextChange: '&',
         onBlur: '&',
         onFocus: '&',
@@ -39,7 +40,9 @@ import { IAugmentedJQuery, ICompileService, IScope } from 'angular';
 })
 export default class SearchBarComponent {
     autoFocus: boolean;
+    inputDebounce: number;
     focused: boolean;
+    onKeyDown: Function;
     onSearchTextChange: Function;
     searchText: string;
 
@@ -52,7 +55,7 @@ export default class SearchBarComponent {
     $onInit(): void {
         this.autoFocus = this.autoFocus !== undefined;
 
-        var self = this;
+        const self = this;
 
         this.$scope.$watch('$ctrl.searchText', (newValue: string, oldValue: string) => {
             if (newValue === oldValue) {
@@ -63,7 +66,7 @@ export default class SearchBarComponent {
         });
     }
 
-    $postLink() {
+    $postLink(): void {
         const self = this;
         if (this.autoFocus) {
             this.$scope.$evalAsync(() => {
@@ -78,7 +81,15 @@ export default class SearchBarComponent {
         this.focusInput();
     }
 
-    focusInput() {
+    focusInput(): void {
         this.$element.find('input')[0].focus();
     }
+
+    onInputKeyDown(event): void {
+        if (event.keyCode === 13) {
+            event.preventDefault();
+        }
+
+        this.onKeyDown({ $event: event });
+    }
 }

+ 1 - 1
src/main/angular/src/ux/table-column.directive.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/ux/table.directive.controller.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 40 - 15
src/main/angular/src/ux/table.directive.html

@@ -1,16 +1,26 @@
-<ng-transclude></ng-transclude>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
 
-<div class="table-configuration">
-    <mf-icon-button icon="settings" ng-click="table.toggleConfigurationVisibility($event)"></mf-icon-button>
-    <ul ng-if="table.showConfiguration" ng-click="$event.stopPropagation()">
-        <li ng-repeat="column in table.columns">
-            <label class="label">
-                <input name="column-visibile" type="checkbox" ng-model="column.visible" aria-label="Toggle column visibility"/>
-                {{column.label}}
-            </label>
-        </li>
-    </ul>
-</div>
+<ng-transclude></ng-transclude>
 
 <table st-table="rowCollection" class="table table-striped">
     <thead>
@@ -18,20 +28,35 @@
             <th ng-repeat="column in table.getVisibleColumns()">
                 <div class="column-header">
                     <span class="label" ng-bind="column.label" ng-click="table.sortOnColumn(column)"></span>
-                    <mf-icon icon="chevron-down"
+                    <mf-icon icon="down_thick"
                              ng-if="table.sortColumn === column && table.reverseSort"
                              ng-click="table.toggleSortOrder()"></mf-icon>
-                    <mf-icon icon="chevron-up"
+                    <mf-icon icon="up_thick"
                              ng-if="table.sortColumn === column && !table.reverseSort"
                              ng-click="table.toggleSortOrder()"></mf-icon>
                 </div>
             </th>
+            <th class="table-configuration-column">
+                <div class="table-configuration">
+                    <mf-icon-button icon="configure_thin" ng-click="table.toggleConfigurationVisibility($event)"></mf-icon-button>
+                    <ul ng-if="table.showConfiguration" ng-click="$event.stopPropagation()">
+                        <li ng-repeat="column in table.columns">
+                            <label class="label">
+                                <input name="column-visibile" type="checkbox" ng-model="column.visible" aria-label="Toggle column visibility"/>
+                                {{column.label}}
+                            </label>
+                        </li>
+                    </ul>
+                </div>
+            </th>
         </tr>
     </thead>
     <tbody>
-        <tr ng-repeat="item in table.getItems()" ng-click="table.clickItem(item, $event)">
+        <tr ng-repeat="item in table.getItems()" ng-click="table.clickItem(item, $event)"
+            ng-attr-id="{{'row-' + $index }}">
             <td ng-repeat="column in table.getVisibleColumns()"
                 ng-bind="table.getValue(item, column.valueExpression)"></td>
+            <td></td>
         </tr>
     </tbody>
 </table>

+ 67 - 40
src/main/angular/src/ux/table.directive.scss

@@ -1,9 +1,9 @@
-/*
+/*!
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -29,38 +29,6 @@ mf-table {
     display: none;
   }
 
-  > .table-configuration {
-    position: relative;
-    z-index: 100;
-
-    > ul {
-      background-color: #ffffff;
-      box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26);
-      box-sizing: border-box;
-      left: 26px;
-      list-style: none;
-      margin: 0;
-      padding: 10px;
-      position: absolute;
-      text-align: start;
-      top: 0;
-
-      > li {
-        height: 24px;
-        line-height: 24px;
-        user-select: none;
-
-        > label {
-          cursor: pointer;
-
-          > input[type="checkbox"] {
-            cursor: inherit;
-          }
-        }
-      }
-    }
-  }
-
   > table {
     border: 1px solid #dae1e1;
     border-collapse: collapse;
@@ -81,7 +49,7 @@ mf-table {
       font-weight: normal;
       overflow: hidden;
       padding: 5px;
-      text-align: start;
+      text-align: left;
       vertical-align: top;
     }
 
@@ -90,6 +58,51 @@ mf-table {
       color: #697c87;
       user-select: none;
 
+      &.table-configuration-column {
+        overflow: visible;
+
+        > .table-configuration {
+          float: right;
+          position: relative;
+          width: 24px;
+          z-index: 100;
+
+          > ul {
+            background-color: #ffffff;
+            box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26);
+            box-sizing: border-box;
+            left: 24px;
+            list-style: none;
+            margin: 0;
+            max-width: 300px;
+            padding: 10px;
+            position: absolute;
+            text-align: left;
+            top: 0;
+            transform: translateX(-100%);
+
+            > li {
+              // Chrome does not show the ellipsis properly for the 'label' element in rtl displays
+              height: 24px;
+              line-height: 24px;
+              user-select: none;
+              display: block;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+
+              > label {
+                cursor: pointer;
+
+                > input[type="checkbox"] {
+                  cursor: inherit;
+                }
+              }
+            }
+          }
+        }
+      }
+
       > .column-header {
         display: flex;
         flex-flow: row nowrap;
@@ -120,11 +133,25 @@ mf-table {
 
 [dir="rtl"] {
   mf-table {
-    > .table-configuration {
-      > ul {
-        left: auto;
-        right: 26px;
+    > table {
+      th, td {
+        text-align: right;
+      }
+
+      th {
+        &.table-configuration-column {
+          > .table-configuration {
+            float: left;
+
+            > ul {
+              text-align: right;
+              left: 0;
+              right: auto;
+              transform: none;
+            }
+          }
+        }
       }
     }
   }
-}
+}

+ 1 - 1
src/main/angular/src/ux/table.directive.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/src/ux/ux.module.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 22 - 0
src/main/angular/vendor/angular-ui-router.js

@@ -1,3 +1,25 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
 /*!
  * State-based routing for AngularJS
  * @version v1.0.0-beta.3

+ 1 - 1
src/main/angular/webpack.build.js

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/webpack.common.js

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/webpack.dev.js

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 1 - 1
src/main/angular/webpack.test.js

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 45 - 11
src/main/java/password/pwm/AppProperty.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -63,11 +63,26 @@ public enum     AppProperty {
     CONFIG_FILE_SCAN_FREQUENCY                      ("config.fileScanFrequencyMS"),
     CONFIG_NEWUSER_PASSWORD_POLICY_CACHE_MS         ("config.newuser.passwordPolicyCacheMS"),
     CONFIG_THEME                                    ("config.theme"),
+    CONFIG_JBCRYPT_PWLIB_ENABLE                     ("config.enableJbCryptPwLibrary"),
     CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT           ("configEditor.queryFilter.testLimit"),
     CONFIG_EDITOR_IDLE_TIMEOUT                      ("configEditor.idleTimeoutSeconds"),
     CONFIG_GUIDE_IDLE_TIMEOUT                       ("configGuide.idleTimeoutSeconds"),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES             ("configManager.zipDebug.maxLogLines"),
     CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS           ("configManager.zipDebug.maxLogSeconds"),
+    CLUSTER_DB_ENABLE                               ("cluster.db.enable"),
+    CLUSTER_DB_HEARTBEAT_SECONDS                    ("cluster.db.heartbeatSeconds"),
+    CLUSTER_DB_NODE_TIMEOUT_SECONDS                 ("cluster.db.nodeTimeoutSeconds"),
+    CLUSTER_DB_NODE_PURGE_SECONDS                   ("cluster.db.nodePurgeSeconds"),
+    DB_JDBC_LOAD_STRATEGY                           ("db.jdbcLoadStrategy"),
+    DB_CONNECTIONS_MAX                              ("db.connections.max"),
+    DB_CONNECTIONS_TIMEOUT_MS                       ("db.connections.timeoutMs"),
+    DB_CONNECTIONS_WATCHDOG_FREQUENCY_SECONDS       ("db.connections.watchdogFrequencySeconds"),
+    DOWNLOAD_FILENAME_STATISTICS_CSV                ("download.filename.statistics.csv"),
+    DOWNLOAD_FILENAME_USER_REPORT_SUMMARY_CSV       ("download.filename.reportSummary.csv"),
+    DOWNLOAD_FILENAME_USER_REPORT_RECORDS_CSV       ("download.filename.reportRecords.csv"),
+    DOWNLOAD_FILENAME_AUDIT_RECORDS_CSV             ("download.filename.auditRecords.csv"),
+    DOWNLOAD_FILENAME_LDAP_PERMISSION_CSV           ("download.filename.ldapPermission.csv"),
+    DOWNLOAD_FILENAME_USER_DEBUG_JSON               ("download.filename.userDebug.json"),
     FORM_EMAIL_REGEX                                ("form.email.regexTest"),
     HTTP_RESOURCES_MAX_CACHE_ITEMS                  ("http.resources.maxCacheItems"),
     HTTP_RESOURCES_MAX_CACHE_BYTES                  ("http.resources.maxCacheBytes"),
@@ -75,7 +90,6 @@ public enum     AppProperty {
     HTTP_RESOURCES_ENABLE_GZIP                      ("http.resources.gzip.enable"),
     HTTP_RESOURCES_ENABLE_PATH_NONCE                ("http.resources.pathNonceEnable"),
     HTTP_RESOURCES_NONCE_PATH_PREFIX                ("http.resources.pathNoncePrefix"),
-    HTTP_RESOURCES_WEBJAR_MAPPINGS                  ("http.resources.webjarMappings"),
     HTTP_RESOURCES_ZIP_FILES                        ("http.resources.zipFiles"),
     HTTP_COOKIE_DEFAULT_SECURE_FLAG                 ("http.cookie.default.secureFlag"),
     HTTP_COOKIE_THEME_NAME                          ("http.cookie.theme.name"),
@@ -127,17 +141,16 @@ public enum     AppProperty {
     HTTP_DOWNLOAD_BUFFER_SIZE                       ("http.download.buffer.size"),
     HTTP_SESSION_RECYCLE_AT_AUTH                    ("http.session.recycleAtAuth"),
     HTTP_SESSION_VALIDATION_KEY_LENGTH              ("http.session.validationKeyLength"),
+    HTTP_SERVLET_ENABLE_POST_REDIRECT_GET           ("http.servlet.enablePostRedirectGet"),
     LOCALDB_AGGRESSIVE_COMPACT_ENABLED              ("localdb.aggressiveCompact.enabled"),
     LOCALDB_IMPLEMENTATION                          ("localdb.implementation"),
     LOCALDB_INIT_STRING                             ("localdb.initParameters"),
+    LOCALDB_LOCATION                                ("localdb.location"),
     LOCALDB_LOGWRITER_BUFFER_SIZE                   ("localdb.logWriter.bufferSize"),
     LOCALDB_LOGWRITER_MAX_BUFFER_WAIT_MS            ("localdb.logWriter.maxBufferWaitMs"),
     LOCALDB_LOGWRITER_MAX_TRIM_SIZE                 ("localdb.logWriter.maxTrimSize"),
     MACRO_RANDOM_CHAR_MAX_LENGTH                    ("macro.randomChar.maxLength"),
     MACRO_LDAP_ATTR_CHAR_MAX_LENGTH                 ("macro.ldapAttr.maxLength"),
-    NAAF_ID                                         ("naaf.id"),
-    NAAF_SECRET                                     ("naaf.secret"),
-    NAAF_SALT_LENGTH                                ("naaf.salt.length"),
 
 
     /** Time intruder records exist in the intruder table before being deleted. */
@@ -149,6 +162,7 @@ public enum     AppProperty {
     INTRUDER_MAX_DELAY_PENALTY_MS                   ("intruder.maximumDelayPenaltyMS"),
     INTRUDER_DELAY_PER_COUNT_MS                     ("intruder.delayPerCountMS"),
     INTRUDER_DELAY_MAX_JITTER_MS                    ("intruder.delayMaxJitterMS"),
+    HEALTHCHECK_ENABLED                             ("healthCheck.enabled"),
     HEALTHCHECK_NOMINAL_CHECK_INTERVAL              ("healthCheck.nominalCheckIntervalSeconds"),
     HEALTHCHECK_MIN_CHECK_INTERVAL                  ("healthCheck.minimumCheckIntervalSeconds"),
     HEALTHCHECK_MAX_RECORD_AGE                      ("healthCheck.maximumRecordAgeSeconds"),
@@ -161,7 +175,14 @@ public enum     AppProperty {
     HELPDESK_TOKEN_VALUE                            ("helpdesk.token.value"),
     HELPDESK_VERIFICATION_INVALID_DELAY_MS          ("helpdesk.verification.invalid.delayMs"),
     HELPDESK_VERIFICATION_TIMEOUT_SECONDS           ("helpdesk.verification.timeoutSeconds"),
+    LDAP_RESOLVE_CANONICAL_DN                       ("ldap.resolveCanonicalDN"),
+    LDAP_CACHE_CANONICAL_ENABLE                     ("ldap.cache.canonical.enable"),
+    LDAP_CACHE_CANONICAL_SECONDS                    ("ldap.cache.canonical.seconds"),
+    LDAP_CACHE_USER_GUID_ENABLE                     ("ldap.cache.userGuid.enable"),
+    LDAP_CACHE_USER_GUID_SECONDS                    ("ldap.cache.userGuid.seconds"),
     LDAP_CHAI_SETTINGS                              ("ldap.chaiSettings"),
+    LDAP_PROXY_CONNECTION_PER_PROFILE               ("ldap.proxy.connectionsPerProfile"),
+    LDAP_PROXY_MAX_CONNECTIONS                      ("ldap.proxy.maxConnections"),
     LDAP_EXTENSIONS_NMAS_ENABLE                     ("ldap.extensions.nmas.enable"),
     LDAP_CONNECTION_TIMEOUT                         ("ldap.connection.timeoutMS"),
     LDAP_PROFILE_RETRY_DELAY                        ("ldap.profile.retryDelayMS"),
@@ -174,6 +195,10 @@ public enum     AppProperty {
     LDAP_BROWSER_MAX_ENTRIES                        ("ldap.browser.maxEntries"),
     LDAP_SEARCH_PAGING_ENABLE                       ("ldap.search.paging.enable"),
     LDAP_SEARCH_PAGING_SIZE                         ("ldap.search.paging.size"),
+    LDAP_SEARCH_PARALLEL_ENABLE                     ("ldap.search.parallel.enable"),
+    LDAP_SEARCH_PARALLEL_FACTOR                     ("ldap.search.parallel.factor"),
+    LDAP_SEARCH_PARALLEL_THREAD_MAX                 ("ldap.search.parallel.threadMax"),
+    LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME        ("ldap.oracle.postTempPasswordUseCurrentTime"),
     LOGGING_PATTERN                                 ("logging.pattern"),
     LOGGING_FILE_MAX_SIZE                           ("logging.file.maxSize"),
     LOGGING_FILE_MAX_ROLLOVER                       ("logging.file.maxRollover"),
@@ -185,6 +210,7 @@ public enum     AppProperty {
     NMAS_THREADS_MIN_SECONDS                        ("nmas.threads.minSeconds"),
     NMAS_THREADS_MAX_SECONDS                        ("nmas.threads.maxSeconds"),
     NMAS_THREADS_WATCHDOG_FREQUENCY                 ("nmas.threads.watchdogFrequencyMs"),
+    NMAS_THREADS_WATCHDOG_DEBUG                     ("nmas.threads.watchdogDebug"),
     NMAS_IGNORE_NMASCR_DURING_FORCECHECK            ("nmas.ignoreNmasCrDuringForceSetupCheck"),
     NMAS_USE_LOCAL_SASL_FACTORY                     ("nmas.useLocalSaslFactory"),
     NMAS_FORCE_SASL_FACTORY_REGISTRATION            ("nmas.forceSaslFactoryRegistration"),
@@ -212,13 +238,13 @@ public enum     AppProperty {
     PASSWORD_RANDOMGEN_MAX_ATTEMPTS                 ("password.randomGenerator.maxAttempts"),
     PASSWORD_RANDOMGEN_MAX_LENGTH                   ("password.randomGenerator.maxLength"),
     PASSWORD_RANDOMGEN_JITTER_COUNT                 ("password.randomGenerator.jitter.count"),
-    PEOPLESEARCH_DISPLAYNAME_USEALLMACROS           ("peoplesearch.displayName.enableAllMacros"),
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ("peoplesearch.values.verifyUserDN"),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ("peoplesearch.values.maxCount"),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ("peoplesearch.view.detail.links"),
     QUEUE_EMAIL_RETRY_TIMEOUT_MS                    ("queue.email.retryTimeoutMs"),
     QUEUE_EMAIL_MAX_AGE_MS                          ("queue.email.maxAgeMs"),
     QUEUE_EMAIL_MAX_COUNT                           ("queue.email.maxCount"),
+    QUEUE_EMAIL_MAX_THREADS                         ("queue.email.maxThreads"),
     QUEUE_SMS_RETRY_TIMEOUT_MS                      ("queue.sms.retryTimeoutMs"),
     QUEUE_SMS_MAX_AGE_MS                            ("queue.sms.maxAgeMs"),
     QUEUE_SMS_MAX_COUNT                             ("queue.sms.maxCount"),
@@ -229,9 +255,13 @@ public enum     AppProperty {
     RECAPTCHA_CLIENT_IFRAME_URL                     ("recaptcha.clientIframeUrl"),
     RECAPTCHA_VALIDATE_URL                          ("recaptcha.validateUrl"),
     REPORTING_LDAP_SEARCH_TIMEOUT                   ("reporting.ldap.searchTimeoutMs"),
+    REPORTING_LDAP_SEARCH_THREADS                   ("reporting.ldap.searchThreads"),
     SECURITY_STRIP_INLINE_JAVASCRIPT                ("security.html.stripInlineJavascript"),
+    SECURITY_HTTP_FORCE_REQUEST_SEQUENCING          ("security.http.forceRequestSequencing"),
     SECURITY_HTTP_STRIP_HEADER_REGEX                ("security.http.stripHeaderRegex"),
+    SECURITY_HTTP_PERFORM_CSRF_HEADER_CHECKS        ("security.http.performCsrfHeaderChecks"),
     SECURITY_HTTP_PROMISCUOUS_ENABLE                ("security.http.promiscuousEnable"),
+    SECURITY_HTTP_CONFIG_CSP_HEADER                 ("security.http.config.cspHeader"),
     SECURITY_HTTPSSERVER_SELF_FUTURESECONDS         ("security.httpsServer.selfCert.futureSeconds"),
     SECURITY_HTTPSSERVER_SELF_ALG                   ("security.httpsServer.selfCert.alg"),
     SECURITY_HTTPSSERVER_SELF_KEY_SIZE              ("security.httpsServer.selfCert.keySize"),
@@ -247,21 +277,25 @@ public enum     AppProperty {
     SECURITY_SHAREDHISTORY_CASE_INSENSITIVE         ("security.sharedHistory.caseInsensitive"),
     SECURITY_SHAREDHISTORY_SALT_LENGTH              ("security.sharedHistory.saltLength"),
     SECURITY_CERTIFICATES_VALIDATE_TIMESTAMPS       ("security.certs.validateTimestamps"),
-    SECURITY_LDAP_BASEDN_RESOLVE_CANONICAL_DN       ("security.ldap.resolveCanonicalDN"),
-    SECURITY_LDAP_BASEDN_CANONICAL_CACHE_SECONDS    ("security.ldap.canonicalCacheSeconds"),
     SECURITY_CONFIG_MIN_SECURITY_KEY_LENGTH         ("security.config.minSecurityKeyLength"),
     SECURITY_DEFAULT_EPHEMERAL_BLOCK_ALG            ("security.defaultEphemeralBlockAlg"),
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ("security.defaultEphemeralHashAlg"),
     SEEDLIST_BUILTIN_PATH                           ("seedlist.builtin.path"),
     SMTP_SUBJECT_ENCODING_CHARSET                   ("smtp.subjectEncodingCharset"),
-    TOKEN_REMOVAL_DELAY_MS                          ("token.removalDelayMS"),
-    TOKEN_PURGE_BATCH_SIZE                          ("token.purgeBatchSize"),
     TOKEN_MAX_UNIQUE_CREATE_ATTEMPTS                ("token.maxUniqueCreateAttempts"),
+    TOKEN_RESEND_ENABLED                            ("token.resend.enabled"),
+    TOKEN_RESEND_DELAY_MS                           ("token.resend.delayMS"),
+    TOKEN_REMOVE_ON_CLAIM                           ("token.removeOnClaim"),
+    TOKEN_VERIFY_PW_MODIFY_TIME                     ("token.verifyPwModifyTime"),
+
 
     /** Regular expression to be used for matching URLs to be shortened by the URL Shortening Service Class. */
     URL_SHORTNER_URL_REGEX                          ("urlshortener.url.regex"),
     WORDLIST_BUILTIN_PATH                           ("wordlist.builtin.path"),
+    WORDLIST_CHAR_LENGTH_MAX                        ("wordlist.maxCharLength"),
+    WORDLIST_CHAR_LENGTH_MIN                        ("wordlist.minCharLength"),
     WS_REST_CLIENT_PWRULE_HALTONERROR               ("ws.restClient.pwRule.haltOnError"),
+    WS_REST_SERVER_SIGNING_FORM_TIMEOUT_SECONDS     ("ws.restServer.signing.form.timeoutSeconds"),
     ALLOW_MACRO_IN_REGEX_SETTING                    ("password.policy.allowMacroInRegexSetting"),
 
     ;
@@ -301,6 +335,6 @@ public enum     AppProperty {
     }
 
     private static String readAppPropertiesBundle(final String key) {
-        return  ResourceBundle.getBundle(AppProperty.class.getName()).getString(key);
+        return ResourceBundle.getBundle(AppProperty.class.getName()).getString(key);
     }
 }

+ 1 - 1
src/main/java/password/pwm/Permission.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by

+ 25 - 12
src/main/java/password/pwm/PwmAboutProperty.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2017 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,13 +24,15 @@ package password.pwm;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
-import password.pwm.util.FileSystemUtility;
-import password.pwm.util.Helper;
 import password.pwm.util.LocaleHelper;
-import password.pwm.util.db.DatabaseAccessor;
+import password.pwm.util.db.DatabaseService;
+import password.pwm.util.java.FileSystemUtility;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 
+import java.nio.charset.Charset;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Map;
@@ -89,6 +91,7 @@ public enum PwmAboutProperty {
     java_osName,
     java_osVersion,
     java_randomAlgorithm,
+    java_defaultCharset,
 
     database_driverName,
     database_driverVersion,
@@ -139,12 +142,16 @@ public enum PwmAboutProperty {
 
         if (pwmApplication.getEmailQueue() != null) {
             aboutMap.put(app_emailQueueSize,       Integer.toString(pwmApplication.getEmailQueue().queueSize()));
-            aboutMap.put(app_emailQueueOldestTime, dateFormatForInfoBean(pwmApplication.getEmailQueue().eldestItem()));
+            if (pwmApplication.getEmailQueue().eldestItem() != null) {
+                aboutMap.put(app_emailQueueOldestTime, dateFormatForInfoBean(Date.from(pwmApplication.getEmailQueue().eldestItem())));
+            }
         }
 
         if (pwmApplication.getSmsQueue() != null) {
             aboutMap.put(app_smsQueueSize,         Integer.toString(pwmApplication.getSmsQueue().queueSize()));
-            aboutMap.put(app_smsQueueOldestTime,   dateFormatForInfoBean(pwmApplication.getSmsQueue().eldestItem()));
+            if (pwmApplication.getSmsQueue().eldestItem() != null) {
+                aboutMap.put(app_smsQueueOldestTime, dateFormatForInfoBean(Date.from(pwmApplication.getSmsQueue().eldestItem())));
+            }
         }
 
         if (pwmApplication.getAuditManager() != null) {
@@ -155,8 +162,8 @@ public enum PwmAboutProperty {
             aboutMap.put(app_localDbLogSize,       Integer.toString(pwmApplication.getLocalDBLogger().getStoredEventCount()));
             aboutMap.put(app_localDbLogOldestTime, dateFormatForInfoBean(pwmApplication.getLocalDBLogger().getTailDate()));
 
-            aboutMap.put(app_localDbStorageSize,   Helper.formatDiskSize(FileSystemUtility.getFileDirectorySize(pwmApplication.getLocalDB().getFileLocation())));
-            aboutMap.put(app_localDbFreeSpace,     Helper.formatDiskSize(FileSystemUtility.diskSpaceRemaining(pwmApplication.getLocalDB().getFileLocation())));
+            aboutMap.put(app_localDbStorageSize,   StringUtil.formatDiskSize(FileSystemUtility.getFileDirectorySize(pwmApplication.getLocalDB().getFileLocation())));
+            aboutMap.put(app_localDbFreeSpace,     StringUtil.formatDiskSize(FileSystemUtility.diskSpaceRemaining(pwmApplication.getLocalDB().getFileLocation())));
         }
 
 
@@ -177,6 +184,7 @@ public enum PwmAboutProperty {
             aboutMap.put(java_osName,              System.getProperty("os.name"));
             aboutMap.put(java_osVersion,           System.getProperty("os.version"));
             aboutMap.put(java_randomAlgorithm,     PwmRandom.getInstance().getAlgorithm());
+            aboutMap.put(java_defaultCharset,      Charset.defaultCharset().name());
         }
 
         { // build info
@@ -192,9 +200,9 @@ public enum PwmAboutProperty {
 
         { // database info
             try {
-                final DatabaseAccessor databaseAccessor = pwmApplication.getDatabaseAccessor();
-                if (databaseAccessor != null) {
-                    final Map<PwmAboutProperty,String> debugData = databaseAccessor.getConnectionDebugProperties();
+                final DatabaseService databaseService = pwmApplication.getDatabaseService();
+                if (databaseService != null) {
+                    final Map<PwmAboutProperty,String> debugData = databaseService.getConnectionDebugProperties();
                     aboutMap.putAll(debugData);
                 }
             } catch (Throwable t) {
@@ -206,11 +214,16 @@ public enum PwmAboutProperty {
     }
 
     private static String dateFormatForInfoBean(final Date date) {
+        return dateFormatForInfoBean(date == null ? null : date.toInstant());
+    }
+
+    private static String dateFormatForInfoBean(final Instant date) {
         if (date != null) {
-            return PwmConstants.DEFAULT_DATETIME_FORMAT.format(date);
+            return date.toString();
         } else {
             return LocaleHelper.getLocalizedMessage(PwmConstants.DEFAULT_LOCALE, Display.Value_NotApplicable, null);
         }
 
     }
+
 }

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません