Ver código fonte

Introduced angular 1 for people search page and org chart functionality

James Albright 8 anos atrás
pai
commit
ebb90c8cbe

+ 1 - 0
.gitignore

@@ -30,3 +30,4 @@
 /src/main/webapp/public/resources/themes/idm
 /src/main/webapp/public/resources/themes/mdefault
 /src/main/webapp/public/resources/themes/netiq
+/src/main/webapp/public/resources/app

+ 95 - 0
pom.xml

@@ -176,6 +176,24 @@
                             </resources>
                         </configuration>
                     </execution>
+                    <execution>
+                        <id>copy-angular-resources</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.basedir}/target/${project.artifactId}-${project.version}/public/resources/app</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/angular/src</directory>
+                                    <excludes>
+                                        <exclude>**/*.ts</exclude>
+                                    </excludes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
                     <execution>
                         <id>replace-build-properties</id>
                         <phase>process-resources</phase>
@@ -339,6 +357,48 @@
                     </dependencyOverrides>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>com.github.eirslett</groupId>
+                <artifactId>frontend-maven-plugin</artifactId>
+                <version>1.0</version>
+                <configuration>
+                    <nodeVersion>v6.6.0</nodeVersion>
+                    <npmVersion>3.10.8</npmVersion>
+                    <installDirectory>target</installDirectory>
+                    <workingDirectory>${basedir}/src/main/angular</workingDirectory>
+                </configuration>
+                <executions>
+                    <!-- install node & npm -->
+                    <execution>
+                        <id>install</id>
+                        <goals>
+                            <goal>install-node-and-npm</goal>
+                        </goals>
+                    </execution>
+
+                    <!-- install package dependencies -->
+                    <execution>
+                        <id>install-packages</id>
+                        <goals>
+                            <goal>npm</goal>
+                        </goals>
+                        <configuration>
+                            <arguments>install</arguments>
+                        </configuration>
+                    </execution>
+
+                    <!-- run TypeScript compiler (You can execute directly using: "mvn frontend:npm@run-tsc") -->
+                    <execution>
+                        <id>run-tsc</id>
+                        <goals>
+                            <goal>npm</goal>
+                        </goals>
+                        <configuration>
+                            <arguments>run compile</arguments>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 
@@ -607,6 +667,41 @@
             <artifactId>famfamfam-flags</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>systemjs</artifactId>
+            <version>0.19.39</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>angular</artifactId>
+            <version>1.5.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>angular-ui-router</artifactId>
+            <version>1.0.0-beta.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>angular-ui-bootstrap</artifactId>
+            <version>2.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>angular-ui-grid</artifactId>
+            <version>3.2.9</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.webjars.npm</groupId>
+                    <artifactId>angular</artifactId>
+                    <!--
+                    angular-ui-grid tries to force an older version of angular.
+                    See: https://github.com/angular-ui/ui-grid/issues/5070
+                    -->
+                </exclusion>
+            </exclusions> 
+        </dependency>
     </dependencies>
 
     <repositories>

+ 2 - 0
src/main/angular/.gitignore

@@ -0,0 +1,2 @@
+/node_modules
+/src/**/*.js

+ 38 - 0
src/main/angular/karma.conf.js

@@ -0,0 +1,38 @@
+//jshint strict: false
+module.exports = function(config) {
+    config.set({
+        frameworks: [ 'jasmine' ],
+
+        basePath: './',
+
+        files: [
+//            '../node_modules/angular/angular.js',
+//            '../node_modules/angular-mocks/angular-mocks.js',
+//            '../node_modules/systemjs/index.js',
+            'src/**/*.test.js'
+        ],
+
+        autoWatch: true,
+
+        browsers: [ 'PhantomJS' ], // PhantomJS, Chrome, Firefox?
+
+        plugins: [
+            'karma-chrome-launcher',
+            'karma-firefox-launcher',
+            'karma-phantomjs-launcher',
+            'karma-jasmine',
+            'karma-junit-reporter',
+            'karma-commonjs-preprocessor'
+        ],
+
+        preprocessors: {
+//            'src/**/*.js': ['commonjs']
+        },
+
+        junitReporter: {
+            outputFile: 'test_out/unit.xml',
+            suite: 'unit'
+        }
+
+    });
+};

+ 34 - 0
src/main/angular/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "pwm",
+  "version": "",
+  "description": "",
+  "main": "index.js",
+  "author": "",
+  "license": "ISC",
+  "dependencies": {},
+  "devDependencies": {
+    "@types/angular": "^1.5.16",
+    "@types/angular-mocks": "^1.5.5",
+    "@types/angular-ui-router": "^1.1.34",
+    "@types/jasmine": "^2.5.35",
+    "angular-mocks": "^1.5.8",
+    "concurrently": "^3.1.0",
+    "cpx": "^1.5.0",
+    "jasmine": "^2.5.2",
+    "karma": "^1.3.0",
+    "karma-chrome-launcher": "^2.0.0",
+    "karma-commonjs": "^1.0.0",
+    "karma-commonjs-preprocessor": "^0.1.1",
+    "karma-firefox-launcher": "^1.0.0",
+    "karma-jasmine": "^1.0.2",
+    "karma-junit-reporter": "^1.1.0",
+    "karma-phantomjs-launcher": "^1.0.2",
+    "systemjs": "^0.19.39",
+    "typescript": "^2.0.3"
+  },
+  "scripts": {
+    "compile": "tsc -p ./src",
+    "sync": "concurrently \"tsc -p ./src -w\" \"cpx ./src/**/*.* ../webapp/public/resources/app -w -v\"",
+    "test": "karma start karma.conf.js"
+  }
+}

+ 55 - 0
src/main/angular/src/orgchart/orgchart.component.html

@@ -0,0 +1,55 @@
+<div id="page-content-title">Organizational Chart</div>
+<div id="orgchart-close" ng-click="$ctrl.close()"></div>
+
+<div id="orgchart-content" class="ng-cloak">
+    <div class="orgchart-primary-person-connector" class="ng-cloak"></div>
+
+    <div class="orgchart-management-title orgchart-title">Management</div>
+    <div class="orgchart-management" >
+        <div class="orgchart-manager" ng-repeat="manager in $ctrl.person.managementChain">
+            <div class="orgchart-separator" ng-class="{first:$first,last:$last}"></div>
+            <div class="orgchart-picture">
+                <img ng-src="{{ manager.pictureUrl }}" />
+            </div>
+            <div class="orgchart-manager-fields">
+                <div class="orgchart-field orgchart-field-0">{{ manager.fields[0].value }}</div>
+                <div class="orgchart-field orgchart-field-1">{{ manager.fields[1].value }}</div>
+            </div>
+        </div>
+    </div>
+
+    <div class="orgchart-primary-person">
+        <img class="orgchart-picture" ng-src="{{ $ctrl.person.pictureUrl }}" />
+        <div class="orgchart-num-reports">{{ $ctrl.person.directReports.length }}</div>
+
+        <div class="orgchart-primary-field orgchart-field-0">{{ $ctrl.person.fields[0].value }}</div>
+        <div class="orgchart-primary-field orgchart-field orgchart-field-1">{{ $ctrl.person.fields[1].value }}</div>
+        <div class="orgchart-primary-field orgchart-field orgchart-field-2 link">{{ $ctrl.person.fields[2].value }}</div>
+        <div class="orgchart-secondary-field orgchart-field orgchart-field-3">
+            <div class="orgchard-field-name">{{ $ctrl.person.fields[3].name }}</div>
+            <div class="orgchard-field-value">{{ $ctrl.person.fields[3].value }}</div>
+        </div>
+        <div class="orgchart-secondary-field orgchart-field orgchart-field-4">
+            <div class="orgchard-field-name">{{ $ctrl.person.fields[4].name }}</div>
+            <div class="orgchard-field-value">{{ $ctrl.person.fields[4].value }}</div>
+        </div>
+        <div class="orgchart-secondary-field orgchart-field orgchart-field-5">
+            <div class="orgchard-field-name">{{ $ctrl.person.fields[5].name }}</div>
+            <div class="orgchard-field-value link">{{ $ctrl.person.fields[5].value }}</div>
+        </div>
+    </div>
+
+    <div class="orgchart-direct-reports-title orgchart-title">Direct Reports</div>
+    <div class="orgchart-direct-reports person-card-list">
+        <div class="person-card" ng-repeat="directReport in $ctrl.person.directReports">
+            <div class="person-card-image"></div>
+            <div class="orgchart-num-reports" ng-if="directReport.numOfReports">{{ directReport.numOfReports }}</div>
+            <div class="person-card-details">
+                <div class="person-card-row-1">{{ directReport.fields[0].value }}</div>
+                <div class="person-card-row-2">{{ directReport.fields[1].value }}</div>
+                <div class="person-card-row-3">{{ directReport.fields[2].value }}</div>
+                <div class="person-card-row-4">{{ directReport.fields[3].value }}</div>
+            </div>
+        </div>
+    </div>
+</div>

+ 16 - 0
src/main/angular/src/orgchart/orgchart.component.test.ts

@@ -0,0 +1,16 @@
+// import 'jasmine';
+// import { OrgChartComponent } from './orgchart.component';
+
+describe('testing OrgChartComponent', () => {
+    beforeEach(() => {
+    });
+
+    it('should pass', () => {
+        expect("foo").toEqual("foo");
+    });
+
+    it('should fail', () => {
+        expect("foo").not.toEqual("bar");
+        expect("foo").not.toEqual("bar");
+    });
+});

+ 29 - 0
src/main/angular/src/orgchart/orgchart.component.ts

@@ -0,0 +1,29 @@
+declare var PWM_GLOBAL: any; // Comes from PWM
+
+export class OrgChartComponent {
+    public templateUrl = PWM_GLOBAL['url-context'] + '/public/resources/app/orgchart/orgchart.component.html';
+
+    public controller = class {
+        private person: any;
+
+        static $inject = ['$state', 'orgChartService'];
+        public constructor(private $state, private orgChartService) {
+        }
+
+        // Available controller life cycle methods are: $onInit, $onDestroy, $onChanges, $postLink
+        public $onInit() {
+            this.orgChartService.getOrgChartData().then((response) => {
+                this.person = response.data.person;
+            }, (response => {
+                console.log(response.data);
+            }));
+        }
+
+        public $onDestroy() {
+        }
+
+        public close() {
+            this.$state.go('search.table');
+        }
+    };
+}

+ 369 - 0
src/main/angular/src/orgchart/orgchart.data.json

@@ -0,0 +1,369 @@
+{
+    "person": {
+        "personId": "1",
+        "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+        "fields": [
+            {
+                "fieldId": "name",
+                "name": "Name",
+                "value": "Andy Smith"
+            }, {
+                "fieldId": "phone",
+                "name": "Phone",
+                "type": "phone",
+                "value": "(123) 456-7890"
+            }, {
+                "fieldId": "email",
+                "name": "Email",
+                "type": "email",
+                "value": "andy.smith@lapd.gov"
+            }, {
+                "fieldId": "title",
+                "name": "Title",
+                "value": "ASST. C/O"
+            }, {
+                "fieldId": "department",
+                "name": "Department",
+                "value": "Office of Operations"
+            }, {
+                "fieldId": "manager",
+                "name": "Manager",
+                "value": "Debra McCarthy",
+                "type": "person-link",
+                "typeMetaData": {
+                    "personId": 3
+                }
+            }
+        ],
+        "managementChain": [
+            {
+                "personId": 3,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Debra McCarthy",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Deputy Chief"
+                    }
+                ]
+            }, {
+                "personId": 4,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Earl Paysinger",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Assistant Chief"
+                    }
+                ]
+            }, {
+                "personId": 5,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Charlie Beck",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Chief of Police"
+                    }
+                ]
+            }, {
+                "personId": 6,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "John W. Mack",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "Commissioner",
+                        "value": "CFO"
+                    }
+                ]
+            }, {
+                "personId": 7,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "R. Tefank",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Executive Director"
+                    }
+                ]
+            }
+        ],
+        "directReports": [
+            {
+                "personId": 8,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 3,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Elmer Davis",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain III"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x001",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "elmer.davis@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 9,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "William Hart",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain I"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x002",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "william.hart@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 10,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 4,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Bob Ginmala",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain III"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x003",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "bob.ginmala@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 11,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 8,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Peter Wittingham",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain I"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x004",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "peter.wittingham@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 12,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Elena Nathan",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain III"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x005",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "elena.nathan@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 13,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Rolphe DeLaTorre",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain I"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x006",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "rolphe.delatorre@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 14,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 19,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Mike Blake",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain III"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "mike.blake@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 15,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 1,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Braeden Crump",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain I"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "braeden.crump@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 16,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Jeremy Peters",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain III"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "jeremy.peters@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 17,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 7,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Jeff West",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain I"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "jeff.west@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 18,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Paul Fontanetta",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain II"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "paul.fontanetta@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }, {
+                "personId": 19,
+                "pictureUrl": "/pwm/public/resources/UserPhoto.png",
+                "numOfReports": 13,
+                "fields": [
+                    {
+                        "fieldId": "name",
+                        "value": "Nadine Lauer",
+                        "type": "person-link"
+                    }, {
+                        "fieldId": "title",
+                        "value": "Captain II"
+                    }, {
+                        "fieldId": "phone",
+                        "value": "(123) 456-7890 x007",
+                        "type": "phone"
+                    }, {
+                        "fieldId": "email",
+                        "value": "nadine.lauer@lapd.gov",
+                        "type": "email"
+                    }
+                ]
+            }
+        ]
+    }
+}

+ 11 - 0
src/main/angular/src/orgchart/orgchart.service.ts

@@ -0,0 +1,11 @@
+declare var PWM_GLOBAL: any; // Comes from PWM
+
+export class OrgChartService {
+    static $inject = ['$http'];
+    public constructor(private $http) {
+    }
+
+    public getOrgChartData() {
+        return this.$http.get(PWM_GLOBAL['url-context'] + '/public/resources/app/orgchart/orgchart.data.json');
+    }
+}

+ 41 - 0
src/main/angular/src/peoplesearch.main.ts

@@ -0,0 +1,41 @@
+/// <reference types="angular" />
+
+// Note: I'd rather use imports for angular rather than the <reference> tag above, but I keep running into problems when angular-ui-router is loaded by SystemJS
+//import "angular";
+//import "angular-ui-router";
+
+import { PeopleSearchService } from "./peoplesearch/peoplesearch.service";
+import { PeopleSearchComponent } from "./peoplesearch/peoplesearch.component";
+import { PeopleSearchTableComponent } from "./peoplesearch/peoplesearch-table.component";
+import { PeopleSearchCardsComponent } from "./peoplesearch/peoplesearch-cards.component";
+import { OrgChartComponent } from "./orgchart/orgchart.component";
+import { OrgChartService } from "./orgchart/orgchart.service";
+
+declare var PWM_PS: any;
+declare var angular: angular.IAngularStatic;
+
+angular.module('PeopleSearchApp', ['ui.router'])
+    .service('peopleSearchService', PeopleSearchService)
+    .service('orgChartService', OrgChartService)
+
+    .component('peopleSearch', new PeopleSearchComponent())
+    .component('peopleSearchTable', new PeopleSearchTableComponent())
+    .component('peopleSearchCards', new PeopleSearchCardsComponent())
+    .component('orgChart', new OrgChartComponent())
+
+    .run(['peopleSearchService', (peopleSearchService) => {
+        // Hack to make the PeopleSearchService available to existing PWM code
+        PWM_PS.peopleSearchService = peopleSearchService;
+    }])
+
+    .config(['$stateProvider', '$urlRouterProvider', ($stateProvider, $urlRouterProvider) => {
+        $urlRouterProvider.otherwise('/search/table');
+
+        $stateProvider.state({ name: 'search', url: '/search', component: 'peopleSearch' });
+        $stateProvider.state({ name: 'search.table', url: '/table', component: 'peopleSearchTable' });
+        $stateProvider.state({ name: 'search.cards', url: '/cards', component: 'peopleSearchCards' });
+        $stateProvider.state({ name: 'orgchart', url: '/orgchart', component: 'orgChart' });
+    }]);
+
+// Attach to the page document
+angular.bootstrap(document, ['PeopleSearchApp']);

+ 11 - 0
src/main/angular/src/peoplesearch/peoplesearch-cards.component.html

@@ -0,0 +1,11 @@
+<div class="person-card-list">
+    <div class="person-card" ng-repeat="person in $ctrl.data" ng-click="$ctrl.selectPerson(person.userKey)">
+        <div class="person-card-image"></div>
+        <div class="person-card-details">
+            <div class="person-card-row-1">{{ person.givenName }} {{ person.sn }}</div>
+            <div class="person-card-row-2">Data Analyst</div>
+            <div class="person-card-row-3">(123) 456-7890</div>
+            <div class="person-card-row-4">{{ person.mail }}</div>
+        </div>
+    </div>
+</div>

+ 30 - 0
src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts

@@ -0,0 +1,30 @@
+declare var PWM_GLOBAL: any;
+declare var PWM_PS: any;
+
+export class PeopleSearchCardsComponent {
+    public templateUrl = PWM_GLOBAL['url-context'] + '/public/resources/app/peoplesearch/peoplesearch-cards.component.html';
+
+    public controller = class {
+        data: any;
+
+        static $inject = ['$scope', '$timeout', 'peopleSearchService'];
+        public constructor(private $scope, private $timeout, private peopleSearchService) {
+        }
+
+        // Available controller life cycle methods are: $onInit, $onDestroy, $onChanges, $postLink
+        public $onInit() {
+            this.peopleSearchService.subscribe(this.$scope, (event, data) => { this.dataChanged(data) });
+        }
+
+        public $onDestroy() {
+        }
+
+        public dataChanged(newData) {
+            this.data = newData;
+        }
+
+        public selectPerson(userKey) {
+            PWM_PS.showUserDetail(userKey);
+        }
+    };
+}

+ 21 - 0
src/main/angular/src/peoplesearch/peoplesearch-table.component.html

@@ -0,0 +1,21 @@
+<table st-table="rowCollection" class="table table-striped">
+    <thead>
+    <tr>
+        <th>First Name</th>
+        <th>Last Name</th>
+        <th>Title</th>
+        <th>Email</th>
+        <th>Telephone</th>
+    </tr>
+    </thead>
+
+    <tbody>
+    <tr ng-repeat="person in $ctrl.data" ng-click="$ctrl.selectPerson(person.userKey)">
+        <td>{{ person.givenName }}</td>
+        <td>{{ person.sn }}</td>
+        <td>{{ person.title }}</td>
+        <td>{{ person.mail }}</td>
+        <td>{{ person.telephone }}</td>
+    </tr>
+    </tbody>
+</table>

+ 30 - 0
src/main/angular/src/peoplesearch/peoplesearch-table.component.ts

@@ -0,0 +1,30 @@
+declare var PWM_GLOBAL: any;
+declare var PWM_PS: any;
+
+export class PeopleSearchTableComponent {
+    public templateUrl = PWM_GLOBAL['url-context'] + '/public/resources/app/peoplesearch/peoplesearch-table.component.html';
+
+    public controller = class {
+        data: any;
+
+        static $inject = ['$scope', '$timeout', 'peopleSearchService'];
+        public constructor(private $scope, private $timeout, private peopleSearchService) {
+        }
+
+        // Available controller life cycle methods are: $onInit, $onDestroy, $onChanges, $postLink
+        public $onInit() {
+            this.peopleSearchService.subscribe(this.$scope, (event, data) => { this.dataChanged(data) });
+        }
+
+        public $onDestroy() {
+        }
+
+        private dataChanged(newData) {
+            this.data = newData;
+        }
+
+        public selectPerson(userKey) {
+            PWM_PS.showUserDetail(userKey);
+        }
+    };
+}

+ 18 - 0
src/main/angular/src/peoplesearch/peoplesearch.component.html

@@ -0,0 +1,18 @@
+<div id="page-content-title">People Search</div>
+
+<div id="panel-searchbar" class="searchbar">
+    <input id="username" name="username" placeholder="Search" class="peoplesearch-input-username" autocomplete="off" /> <!-- Auto focus this control -->
+    <div class="searchbar-extras">
+        <div id="searchIndicator" style="display: none">
+            <span style="" class="pwm-icon pwm-icon-lg pwm-icon-spin pwm-icon-spinner"></span>
+        </div>
+
+        <div id="maxResultsIndicator" style="display: none;">
+            <span style="color: #ffcd59;" class="pwm-icon pwm-icon-lg pwm-icon-exclamation-circle"></span>
+        </div>
+    </div>
+</div>
+
+<div id="people-search-view-toggle" ng-click="$ctrl.viewToggleClicked()" ng-class="$ctrl.viewToggleClass"></div>
+
+<ui-view id="people-search-component-view">Loading...</ui-view>

+ 35 - 0
src/main/angular/src/peoplesearch/peoplesearch.component.ts

@@ -0,0 +1,35 @@
+declare var PWM_GLOBAL: any;
+
+export class PeopleSearchComponent {
+    public templateUrl = PWM_GLOBAL['url-context'] + '/public/resources/app/peoplesearch/peoplesearch.component.html';
+
+    public controller = class {
+        viewToggleClass: string;
+
+        static $inject = ['$state'];
+        public constructor(private $state) {
+        }
+
+        // Available controller life cycle methods are: $onInit, $onDestroy, $onChanges, $postLink
+        public $onInit() {
+            if (this.$state.is('search.table')) {
+                this.viewToggleClass = 'fa fa-th-large';
+            } else {
+                this.viewToggleClass = 'fa fa-list-alt';
+            }
+        }
+
+        public $onDestroy() {
+        }
+
+        private viewToggleClicked() {
+            if (this.$state.is('search.table')) {
+                this.$state.go('search.cards');
+                this.viewToggleClass = 'fa fa-list-alt';
+            } else {
+                this.$state.go('search.table');
+                this.viewToggleClass = 'fa fa-th-large';
+            }
+        }
+    };
+}

+ 26 - 0
src/main/angular/src/peoplesearch/peoplesearch.service.ts

@@ -0,0 +1,26 @@
+export class PeopleSearchService {
+    private data: any;
+
+    static $inject = ['$rootScope', '$timeout'];
+    public constructor(private $rootScope, private $timeout) {
+    }
+
+    public subscribe(subscribersScope, callback) {
+        var deregistrationCallback = this.$rootScope.$on('peoplesearch-data-changed', callback);
+        subscribersScope.$on('$destroy', deregistrationCallback);
+
+        if (this.data) {
+            this.$timeout(() => this.notifyPeoplesearchDataChangedListeners(this.data));
+        }
+    }
+
+    private notifyPeoplesearchDataChangedListeners(data: any) {
+        this.$rootScope.$emit('peoplesearch-data-changed', data);
+        this.$rootScope.$apply();
+    }
+
+    public updateData(data: any) {
+        this.data = data;
+        this.notifyPeoplesearchDataChangedListeners(data);
+    }
+}

+ 16 - 0
src/main/angular/src/tsconfig.json

@@ -0,0 +1,16 @@
+{
+    "compilerOptions": {
+        "declaration": false,
+        "emitDecoratorMetadata": true,
+        "experimentalDecorators": true,
+        "module": "commonjs",
+        "moduleResolution": "node",
+        "noEmitOnError": true,
+        "noImplicitAny": false,
+        "sourceMap": false,
+        "target": "es5",
+        "typeRoots": [
+            "../node_modules/@types"
+        ]
+    }
+}

+ 1 - 1
src/main/resources/password/pwm/AppProperty.properties

@@ -79,7 +79,7 @@ http.resources.expirationSeconds=3600
 http.resources.gzip.enable=true
 http.resources.pathNonceEnable=true
 http.resources.pathNoncePrefix=nonce-
-http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.6.3/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/"}
+http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.6.3/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/","/public/resources/systemjs/":"/webjars/systemjs/0.19.39/","/public/resources/angular/":"/webjars/angular/1.5.8/","/public/resources/angular-ui-router/":"/webjars/angular-ui-router/1.0.0-beta.3/","/public/resources/angular-ui-bootstrap/":"/webjars/angular-ui-bootstrap/2.1.4/","/public/resources/angular-ui-grid/":"/webjars/angular-ui-grid/3.2.9/"}
 http.resources.zipFiles=[]
 http.gzip.enable=true
 http.errors.allowHtml=true

+ 28 - 22
src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

@@ -31,32 +31,38 @@
         <jsp:param name="pwm.PageName" value="Title_PeopleSearch"/>
     </jsp:include>
     <div id="centerbody" class="wide tall" style="height:100%">
-        <div id="page-content-title"><pwm:display key="Title_PeopleSearch" displayIfMissing="true"/></div>
-
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
-        <div id="panel-searchbar" class="searchbar">
-	        <input id="username" name="username" placeholder="<pwm:display key="Placeholder_Search"/>" class="peoplesearch-input-username" <pwm:autofocus/> autocomplete="off" />
-            <div class="searchbar-extras">
-                <div id="searchIndicator" style="display: none">
-                    <span style="" class="pwm-icon pwm-icon-lg pwm-icon-spin pwm-icon-spinner"></span>
-                </div>
-
-                <div id="maxResultsIndicator" style="display: none;">
-                    <span style="color: #ffcd59;" class="pwm-icon pwm-icon-lg pwm-icon-exclamation-circle"></span>
-                </div>
-            </div>
-
-            <noscript>
-                <span><pwm:display key="Display_JavascriptRequired"/></span>
-                <a href="<pwm:context/>"><pwm:display key="Title_MainPage"/></a>
-            </noscript>
-        </div>
-        <div id="peoplesearch-searchResultsGrid" class="searchResultsGrid grid tall">
-        </div>
+
+        <ui-view id="people-search-view">Loading...</ui-view>
     </div>
     <div class="push"></div>
 </div>
+
+<%-- TODO: change these to the 'min' versions (i.e. angular.min.js) --%>
+<pwm:script-ref url="/public/resources/systemjs/dist/system.js" />
+
+<%--
+Note: I'd rather access angular from the typescript code using: import "angular", and import "angular-ui-router", but
+angular-ui-router kept having problems, so I just hard coded the paths in script tags here: --%>
+<pwm:script-ref url="/public/resources/angular/angular.js" />
+<pwm:script-ref url="/public/resources/angular-ui-router/release/angular-ui-router.js" />
+
 <%@ include file="fragment/footer.jsp" %>
-<pwm:script-ref url="/public/resources/js/peoplesearch.js"/>
+<pwm:script-ref url="/public/resources/js/peoplesearch.js" />
+
+<pwm:script>
+    <script>
+    System.config({
+        defaultJSExtensions: true,
+        map: {
+            "angular": "<pwm:url addContext='true' url='/public/resources/angular/angular.js' />",
+            "angular-ui-router": "<pwm:url addContext='true' url='/public/resources/angular-ui-router/release/angular-ui-router.js' />"
+        }
+    });
+
+    System.import("<pwm:url addContext='true' url='/public/resources/app/peoplesearch.main.js' />");
+    </script>
+</pwm:script>
+
 </body>
 </html>

+ 31 - 27
src/main/webapp/public/resources/js/peoplesearch.js

@@ -41,18 +41,20 @@ PWM_PS.processPeopleSearch = function() {
         PWM_MAIN.getObject('searchIndicator').style.display = 'none';
     };
     validationProps['processResultsFunction'] = function(data) {
-        var grid = PWM_VAR['peoplesearch_search_grid'];
+        if (PWM_PS.peopleSearchService) PWM_PS.peopleSearchService.updateData(data['data']['searchResults']);
+
+//        var grid = PWM_VAR['peoplesearch_search_grid'];
         if (data['error']) {
-            grid.refresh();
+//            grid.refresh();
             PWM_MAIN.showErrorDialog(data);
         } else {
 
             var gridData = data['data']['searchResults'];
             var sizeExceeded = data['data']['sizeExceeded'];
-            grid.refresh();
-            grid.renderArray(gridData);
-            var sortState = grid.get("sort");
-            grid.set("sort", sortState);
+//            grid.refresh();
+//            grid.renderArray(gridData);
+//            var sortState = grid.get("sort");
+//            grid.set("sort", sortState);
 
 
             if (sizeExceeded) {
@@ -169,7 +171,9 @@ PWM_PS.applyEventHandlersToDetailView = function(data) {
 
     if (data['hasOrgChart']) {
         PWM_MAIN.addEventHandler('button-peoplesearch-orgChart', 'click', function () {
-            PWM_PS.showOrgChartView(data['userKey']);
+            PWM_MAIN.goto(PWM_GLOBAL['url-context'] + '/private/peoplesearch#/orgchart');
+            PWM_MAIN.clearDijitWidget('dialogPopup');
+//            PWM_PS.showOrgChartView(data['userKey']);
         });
     }
 
@@ -369,30 +373,30 @@ PWM_PS.showOrgChartView = function(userKey, asParent) {
 PWM_PS.makeSearchGrid = function(nextFunction) {
     require(["dojo","dojo/_base/declare", "dgrid/Grid", "dgrid/Keyboard", "dgrid/Selection", "dgrid/extensions/ColumnResizer", "dgrid/extensions/ColumnReorder", "dgrid/extensions/ColumnHider", "dojo/domReady!"],
         function(dojo,declare, Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider){
-            PWM_MAIN.getObject('peoplesearch-searchResultsGrid').innerHTML = '';
+//            PWM_MAIN.getObject('peoplesearch-searchResultsGrid').innerHTML = '';
 
             var CustomGrid = declare([ Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider ]);
 
-            PWM_VAR['peoplesearch_search_grid'] = new CustomGrid({
-                columns: PWM_VAR['peoplesearch_search_columns'],
-                queryOptions: {
-                    sort: [{ attribute: "sn" }]
-                }
-            }, "peoplesearch-searchResultsGrid");
+//            PWM_VAR['peoplesearch_search_grid'] = new CustomGrid({
+//                columns: PWM_VAR['peoplesearch_search_columns'],
+//                queryOptions: {
+//                    sort: [{ attribute: "sn" }]
+//                }
+//            }, "peoplesearch-searchResultsGrid");
 
             if (nextFunction) {
                 nextFunction();
             }
 
-            PWM_VAR['peoplesearch_search_grid'].on(".dgrid-row:click", function(evt){
-                PWM_MAIN.stopEvent(evt);
-                evt.preventDefault();
-                var row = PWM_VAR['peoplesearch_search_grid'].row(evt);
-                var userKey = row.data['userKey'];
-                PWM_PS.showUserDetail(userKey);
-            });
+//            PWM_VAR['peoplesearch_search_grid'].on(".dgrid-row:click", function(evt){
+//                PWM_MAIN.stopEvent(evt);
+//                evt.preventDefault();
+//                var row = PWM_VAR['peoplesearch_search_grid'].row(evt);
+//                var userKey = row.data['userKey'];
+//                PWM_PS.showUserDetail(userKey);
+//            });
 
-            PWM_VAR['peoplesearch_search_grid'].set("sort", { attribute : 'sn', descending: true});
+//            PWM_VAR['peoplesearch_search_grid'].set("sort", { attribute : 'sn', descending: true});
 
         }
     );
@@ -418,7 +422,7 @@ PWM_PS.initPeopleSearchPage = function() {
     var applicationData = PWM_MAIN.getObject("application-info");
     var jspName = applicationData ? applicationData.getAttribute("data-jsp-name") : "";
     if ("peoplesearch.jsp" == jspName) {
-        var url = PWM_MAIN.addParamToUrl(window.location.href,'processAction','clientData');
+        var url = PWM_MAIN.addParamToUrl(window.location.href.replace(window.location.hash, ''),'processAction','clientData');
         PWM_MAIN.ajaxRequest(url,function(data){
             if (data['error']) {
                 PWM_MAIN.showErrorDialog(data);
@@ -447,8 +451,10 @@ PWM_PS.initPeopleSearchPage = function() {
 
                     PWM_PS.processPeopleSearch();
                 });
-                if (PWM_MAIN.getObject('username').value && PWM_MAIN.getObject('username').value.length > 0) {
-                    PWM_PS.processPeopleSearch();
+                if (PWM_MAIN.getObject('username')) {
+                    if (PWM_MAIN.getObject('username').value && PWM_MAIN.getObject('username').value.length > 0) {
+                        PWM_PS.processPeopleSearch();
+                    }
                 }
             });
         },{method:"GET"});
@@ -456,5 +462,3 @@ PWM_PS.initPeopleSearchPage = function() {
 };
 
 PWM_PS.initPeopleSearchPage();
-
-

+ 365 - 0
src/main/webapp/public/resources/style.css

@@ -1056,3 +1056,368 @@ dialog .closeIcon { float: right; cursor: pointer; margin-right: 3px; }
 #grid-hider-menu {
     width: 200px;
 }
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+  display: none !important;
+}
+
+#people-search-view {
+    bottom: 0;
+    display: block;
+    left: 0;
+    margin: 5px;
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+#people-search-component-view {
+    bottom: 0;
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 85px;
+}
+
+#people-search-view-toggle {
+    border: 1px solid transparent;
+    color: #808080;
+    cursor: pointer;
+    font-size: 20px;
+    height: 24px;
+    position: absolute;
+    right: 0;
+    top: 42px;
+    width: 24px;
+}
+
+#people-search-view-toggle.fa::before {
+    left: 2px;
+    position: absolute;
+    top: 2px;
+}
+
+#people-search-view-toggle:hover {
+    background-color: #f6f9f8;
+    border: 1px solid #dae1e1;
+    color: #0088ce;
+}
+
+people-search-table {
+    border: 1px solid #dae1e1;
+    bottom: 0;
+    left: 0;
+    overflow: auto;
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+people-search-table table {
+    border: 0 none;
+    width: 100%;
+}
+
+people-search-table table th, people-search-table table td {
+    border-bottom: 1px solid #dae1e1;
+    font-weight: normal;
+    overflow: hidden;
+    padding: 5px;
+    text-align: left;
+    vertical-align: top;
+}
+
+people-search-table table th {
+    background-color: #eee;
+    color: #697c87;
+}
+
+people-search-cards {
+    bottom: 0;
+    left: 0;
+    overflow: auto;
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+.person-card-list .person-card {
+    background-color: #eef2f2;
+    border: 1px solid #eef2f2;
+    display: inline-block;
+    height: 60px;
+    margin-right: 5px;
+    margin-top: 5px;
+    padding: 10px;
+    vertical-align: top;
+    width: 200px;
+}
+
+.person-card-list .person-card:hover {
+    background: #f6f9f8 none repeat scroll 0 0;
+    border: 1px solid #28a9e1 !important;
+    cursor: pointer;
+}
+
+.person-card-list .person-card {
+    position: relative;
+}
+
+.person-card-list .person-card .person-card-image {
+    background: gray url("UserPhoto.png") no-repeat scroll 0 0 / 50px 50px;
+    height: 50px;
+    position: absolute;
+    top: 10px;
+    width: 50px;
+}
+
+.person-card-list .person-card .person-card-details {
+    left: 70px;
+    position: absolute;
+    top: 10px;
+}
+
+.person-card-row-1 {
+    color: black;
+    font-size: 14px;
+}
+
+.person-card-row-2, .person-card-row-3, .person-card-row-4 {
+    color: #808080;
+    font-size: 12px;
+}
+
+org-chart {
+    bottom: 0;
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+#orgchart-content {
+    bottom: 0;
+    left: 0;
+    overflow: auto;
+    position: absolute;
+    right: 0;
+    top: 42px;
+}
+
+.orgchart-primary-person {
+    background: #fff none repeat scroll 0 0;
+    border: 3px solid #808080;
+    border-radius: 3px;
+    float: none;
+    height: 160px;
+    margin: 20px 0 0;
+    position: relative;
+    width: 340px;
+}
+
+.orgchart-primary-person {
+    margin-left: 120px;
+}
+
+.orgchart-primary-person .orgchart-picture {
+    height: 65px;
+    left: 10px;
+    position: absolute;
+    top: 10px;
+    width: 65px;
+}
+
+.orgchart-num-reports {
+    background-color: #dae1e1;
+    border-radius: 2px;
+    color: #434c50;
+    font-size: 12px;
+    height: 18px;
+    line-height: 18px;
+    margin-bottom: 0;
+    padding-bottom: 0;
+    position: absolute;
+    right: 3px;
+    text-align: center;
+    top: 3px;
+    width: 25px;
+}
+
+.orgchart-primary-person .orgchart-primary-field {
+    color: #808080;
+    left: 90px;
+    font-size: 13px;
+    position: absolute;
+}
+
+.orgchart-primary-person .orgchart-primary-field.link,
+.orgchart-primary-person .orgchard-field-value.link {
+    color: #0088ce;
+}
+
+.orgchart-primary-person .orgchart-field-0 {
+    color: black;
+    font-size: 14px;
+    top: 10px;
+}
+
+.orgchart-primary-person .orgchart-field-1 {
+    top: 28px;
+}
+
+.orgchart-primary-person .orgchart-field-2 {
+    top: 46px;
+}
+
+.orgchart-primary-person .orgchart-secondary-field {
+    position: absolute;
+    left: 10px;
+}
+
+.orgchart-primary-person .orgchart-secondary-field .orgchard-field-name {
+    color: #808080;
+    display: inline-block;
+    font-size: 13px;
+    overflow: hidden;
+    width: 100px;
+}
+
+.orgchart-primary-person .orgchart-secondary-field .orgchard-field-value {
+    display: inline-block;
+    font-size: 13px;
+    overflow: hidden;
+    width: 215px;
+}
+
+.orgchart-primary-person .orgchart-field-3 {
+    border-top: 1px solid #dae1e1;
+    padding-top: 8px;
+    top: 80px;
+}
+
+.orgchart-primary-person .orgchart-field-4 {
+    top: 111px;
+}
+
+.orgchart-primary-person .orgchart-field-5 {
+    top: 134px;
+}
+
+.orgchart-direct-reports .num-reports {
+    position: absolute;
+    right: 3px;
+    top: 3px;
+}
+
+.orgchart-direct-reports {
+    border-top: 3px solid #808080;
+    margin-right: 25px;
+    margin-top: 40px;
+    min-width: 700px;
+    padding-top: 0;
+}
+
+.orgchart-management {
+    margin-left: 90px;
+    min-width: 650px;
+}
+
+.orgchart-management .orgchart-manager {
+    display: inline-block;
+    padding-top: 75px;
+    position: relative;
+    width: 130px;
+}
+
+.orgchart-manager-fields {
+    background-color: white;
+    padding: 4px;
+}
+
+.orgchart-manager .orgchart-separator {
+    background-color: #dae1e1;
+    height: 3px;
+    position: absolute;
+    top: 40px;
+    width: 100%;
+}
+
+.orgchart-primary-person-connector {
+    background-color: #808080;
+    height: 290px;
+    left: 155px;
+    position: absolute;
+    top: 50px;
+    width: 5px;
+}
+
+.orgchart-manager .orgchart-separator.first {
+    margin-left: 50%;
+    width: 50%;
+}
+
+.orgchart-manager .orgchart-separator.last {
+    margin-right: 50%;
+    width: 50%;
+}
+
+.orgchart-manager .orgchart-separator.first.last {
+    display: none;
+}
+
+.orgchart-manager .orgchart-field {
+    color: #808080;
+    font-size: 12px;
+    text-align: center;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.orgchart-manager .orgchart-picture {
+    left: 35px;
+    margin: 0 auto;
+    position: absolute;
+    top: 10px;
+    width: 50%;
+}
+
+.orgchart-manager .orgchart-picture img {
+    border: 3px solid #dae1e1;
+    border-radius: 50%;
+    height: 50px;
+    margin: 3px;
+    width: 50px;
+}
+
+#orgchart-close {
+    background: transparent url("close_40.png") no-repeat scroll 0 0 / 20px 20px;
+    border: 1px solid transparent;
+    height: 20px;
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 20px;
+}
+
+#orgchart-close:hover {
+    background-color: #f6f9f8;
+    border: 1px solid #dae1e1;
+    color: #0088ce;
+}
+
+.orgchart-title {
+    color: #808080;
+    font-size: 14px;
+    left: 0;
+    position: absolute;
+}
+
+.orgchart-management-title {
+    top: 25px;
+}
+
+.orgchart-direct-reports-title {
+    top: 315px;
+}
+