Merge pull request #4486 from ssilvert/kc1250-big-commit

KEYCLOAK-1250: Initial commit for new account mgt.
This commit is contained in:
Stan Silvert 2017-09-18 16:51:18 -04:00 committed by GitHub
commit 657c68475d
91 changed files with 8159 additions and 0 deletions

View file

@ -49,6 +49,62 @@
</resources> </resources>
</build> </build>
</profile> </profile>
<profile>
<id>account2</id>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<executions>
<execution>
<id>yarn install keycloak preview</id>
<goals>
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install --production=false --frozen-lockfile</arguments>
<workingDirectory>src/main/resources/theme/keycloak-preview/account/resources</workingDirectory>
</configuration>
</execution>
<execution>
<id>compile typescript</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>compile</phase>
<configuration>
<arguments>run build</arguments>
<workingDirectory>src/main/resources/theme/keycloak-preview/account/resources</workingDirectory>
</configuration>
</execution>
<execution>
<id>yarn remove dev dependencies production=true</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>process-classes</phase>
<configuration>
<arguments>install --production=true --frozen-lockfile</arguments>
<workingDirectory>src/main/resources/theme/keycloak-preview/account/resources</workingDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<configuration>
<filesets>
<fileset>
<directory>src/main/resources/theme/keycloak-preview/account/resources/node_modules</directory>
</fileset>
</filesets>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles> </profiles>
<build> <build>

View file

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html class="layout-pf-alt layout-pf-alt-fixed">
<head>
<title>Keycloak Account</title>
<script>
var authUrl = '${authUrl}';
var baseUrl = '${baseUrl}';
var realm = '${realm}';
var resourceUrl = '${resourceUrl}';
<#if referrer??>
var referrer = '${referrer}';
var referrer_uri = '${referrer_uri?html}';
</#if>
<#if msg??>
var locale = '${locale}';
var l18n_msg = JSON.parse('${msg}');
</#if>
</script>
<base href="${baseUrl}/">
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="${resourceUrl}/app/assets/img/favicon.ico" type="image/x-icon"/>
<#if properties.styles?has_content>
<#list properties.styles?split(' ') as style>
<link href="${resourceUrl}/${style}" rel="stylesheet"/>
</#list>
</#if>
<link rel="stylesheet" href="${resourceUrl}/styles.css">
<!--<script src="${authUrl}/js/${resourceVersion}/keycloak.js" type="text/javascript"></script>-->
<!-- PatternFly -->
<!-- iPad retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="152x152"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-152.png">
<!-- iPad retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="144x144"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-144.png">
<!-- iPad non-retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="76x76"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-76.png">
<!-- iPad non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="72x72"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-72.png">
<!-- iPhone 6 Plus icon -->
<link rel="apple-touch-icon-precomposed" sizes="120x120"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-180.png">
<!-- iPhone retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="114x114"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-114.png">
<!-- iPhone non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="57x57"
href="${resourceUrl}/node_modules/patternfly/dist/img/apple-touch-icon-precomposed-57.png">
<link href="${resourceUrl}/node_modules/patternfly/dist/css/patternfly.min.css" rel="stylesheet"
media="screen, print">
<link href="${resourceUrl}/node_modules/patternfly/dist/css/patternfly-additions.min.css" rel="stylesheet"
media="screen, print">
<script src="${resourceUrl}/node_modules/jquery/dist/jquery.min.js"></script>
<script src="${resourceUrl}/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="${resourceUrl}/node_modules/jquery-match-height/dist/jquery.matchHeight-min.js"></script>
<script src="${resourceUrl}/node_modules/patternfly/dist/js/patternfly.min.js"></script>
<!-- Polyfill(s) for older browsers -->
<script src="${resourceUrl}/node_modules/core-js/client/shim.min.js"></script>
<#if properties.scripts?has_content>
<#list properties.scripts?split(' ') as script>
<script type="text/javascript" src="${resourceUrl}/${script}"></script>
</#list>
</#if>
<script src="${resourceUrl}/node_modules/zone.js/dist/zone.js"></script>
<script src="${resourceUrl}/node_modules/systemjs/dist/system.src.js"></script>
<script src="${resourceUrl}/systemjs.config.js"></script>
<script>
System.import('${resourceUrl}/main.js').catch(function (err) {
console.error(err);
});
</script>
</head>
<app-root>
<style>
.kc-background {
background: url('${resourceUrl}/img/keycloak-bg.png') top left no-repeat;
background-size: cover;
}
.logo-centered {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.kc-logo-text {
background-image: url("${resourceUrl}/img/keycloak-logo-text.png");
background-repeat: no-repeat;
width: 250px;
height: 38px;
}
</style>
<body class="cards-pf kc-background">
<div class='logo-centered kc-logo-text'/>
</body>
</app-root>
</html>

View file

@ -0,0 +1,19 @@
# ignore typescript-generated files
*.js
*.js.map
# ignore keycloak.json - we won't need this much longer
keycloak.json
# ignore log files
*.log
# ignore libraries (for now?)
node_modules
node
# Don't ignore these
!keycloak.js
!systemjs-angular-loader.js
!systemjs.config.extras.js
!systemjs.config.js

View file

@ -0,0 +1,110 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Injectable} from '@angular/core';
import {Http, Response, RequestOptionsArgs} from '@angular/http';
import {ToastNotifier, ToastNotification} from '../top-nav/toast.notifier';
import {KeycloakService} from '../keycloak-service/keycloak.service';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Injectable()
export class AccountServiceClient {
private accountUrl: string;
constructor(protected http: Http,
protected kcSvc: KeycloakService,
protected notifier: ToastNotifier) {
this.accountUrl = kcSvc.authServerUrl() + 'realms/' + kcSvc.realm() + '/account';
}
public doGetRequest(endpoint: string,
responseHandler: Function,
options?: RequestOptionsArgs) {
this.http.get(this.accountUrl + endpoint, options)
.subscribe((res: Response) => responseHandler(res),
(error: Response) => this.handleServiceError(error));
}
public doPostRequest(endpoint: string,
responseHandler: Function,
options?: RequestOptionsArgs,
successMessage?: string) {
this.http.post(this.accountUrl + endpoint, options)
.subscribe((res: Response) => this.handleAccountUpdated(responseHandler, res, successMessage),
(error: Response) => this.handleServiceError(error));
}
private handleAccountUpdated(responseHandler: Function, res: Response, successMessage?: string) {
let message: string = "Your account has been updated.";
if (successMessage) message = successMessage;
this.notifier.emit(new ToastNotification(message, "success"));
responseHandler(res);
}
public doDelete(endpoint: string,
responseHandler: Function,
options?: RequestOptionsArgs,
successMessage?: string) {
this.http.delete(this.accountUrl + endpoint, options)
.subscribe((res: Response) => this.handleAccountUpdated(responseHandler, res, successMessage),
(error: Response) => this.handleServiceError(error));
}
private handleServiceError(response: Response): void {
console.log('**** ERROR!!!! ***');
console.log(JSON.stringify(response));
console.log("response.status=" + response.status);
console.log('***************************************')
if ((response.status === undefined) || (response.status === 401)) {
this.kcSvc.logout();
return;
}
if (response.status === 403) {
// TODO: go to a forbidden page?
}
if (response.status === 404) {
// TODO: route to PageNotFoundComponent
}
let message: string = response.status + " " + response.statusText;
const not500Error: boolean = response.status !== 500;
console.log('not500Error=' + not500Error);
// Unfortunately, errors can be sent back in the response body as
// 'errorMessage' or 'error_description'
if (not500Error && response.json().hasOwnProperty('errorMessage')) {
message = response.json().errorMessage;
}
if (not500Error && response.json().hasOwnProperty('error_description')) {
message = response.json().error_description;
}
this.notifier.emit(new ToastNotification(message, "error"));
}
}

View file

@ -0,0 +1,9 @@
<app-top-nav></app-top-nav>
<app-side-nav></app-side-nav>
<div class="container-fluid container-pf-alt-nav-pf-vertical-alt {{this.contentWidthClass}}"> <!-- collapsed-nav hidden-nav -->
<router-outlet></router-outlet>
</div>

View file

@ -0,0 +1,53 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, HostListener} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';
import {ResponsivenessService, ContentWidthClass, MenuClickListener} from "./responsiveness-service/responsiveness.service";
declare const locale: string;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements MenuClickListener {
private contentWidthClass: ContentWidthClass = this.respSvc.calcSideContentWidthClass();
constructor(translate: TranslateService, private respSvc: ResponsivenessService) {
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use(locale);
this.respSvc.addMenuClickListener(this);
}
public menuClicked() : void {
this.contentWidthClass = this.respSvc.calcSideContentWidthClass();
}
@HostListener('window:resize', ['$event'])
private onResize(event: any) {
this.contentWidthClass = this.respSvc.calcSideContentWidthClass();
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule, Routes } from '@angular/router';
import { LocationStrategy, HashLocationStrategy } from '@angular/common';
import { TranslateLoader } from '@ngx-translate/core';
import { TranslateModule } from '@ngx-translate/core';
import { KeycloakService } from './keycloak-service/keycloak.service';
import { KEYCLOAK_HTTP_PROVIDER } from './keycloak-service/keycloak.http';
import {ResponsivenessService} from './responsiveness-service/responsiveness.service'
import { AccountServiceClient } from './account-service/account.service';
import {TranslateUtil} from './ngx-translate/translate.util';
import { DeclaredVarTranslateLoader } from './ngx-translate/declared.var.translate.loader';
import { AppComponent } from './app.component';
import { TopNavComponent } from './top-nav/top-nav.component';
import { NotificationComponent } from './top-nav/notification.component';
import { ToastNotifier } from './top-nav/toast.notifier';
import { SideNavComponent } from './side-nav/side-nav.component';
import { AccountPageComponent } from './content/account-page/account-page.component';
import { PasswordPageComponent } from './content/password-page/password-page.component';
import { PageNotFoundComponent } from './content/page-not-found/page-not-found.component';
import { AuthenticatorPageComponent } from './content/authenticator-page/authenticator-page.component';
import { SessionsPageComponent } from './content/sessions-page/sessions-page.component';
import { LargeSessionCardComponent } from './content/sessions-page/large-session-card.component';
import { SmallSessionCardComponent } from './content/sessions-page/small-session-card.component';
import { ApplicationsPageComponent } from './content/applications-page/applications-page.component';
import { LargeAppCardComponent } from './content/applications-page/large-app-card.component';
import { SmallAppCardComponent } from './content/applications-page/small-app-card.component';
import { RowAppCardComponent } from './content/applications-page/row-app-card.component';
import { ToolbarComponent } from './content/widgets/toolbar.component';
import {OrderbyPipe} from './pipes/orderby.pipe';
import {FilterbyPipe} from './pipes/filterby.pipe';
const routes: Routes = [
{ path: 'account', component: AccountPageComponent },
{ path: 'password', component: PasswordPageComponent },
{ path: 'authenticator', component: AuthenticatorPageComponent },
{ path: 'sessions', component: SessionsPageComponent },
{ path: 'applications', component: ApplicationsPageComponent },
{ path: '', redirectTo: '/account', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent}
];
const decs = [
AppComponent,
TopNavComponent,
NotificationComponent,
SideNavComponent,
AccountPageComponent,
PasswordPageComponent,
PageNotFoundComponent,
AuthenticatorPageComponent,
SessionsPageComponent,
LargeSessionCardComponent,
SmallSessionCardComponent,
ApplicationsPageComponent,
LargeAppCardComponent,
SmallAppCardComponent,
RowAppCardComponent,
ToolbarComponent,
OrderbyPipe,
FilterbyPipe
];
export const ORIGINAL_INCOMING_URL: Location = window.location;
@NgModule({
declarations: decs,
imports: [
BrowserModule,
FormsModule,
HttpModule,
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: DeclaredVarTranslateLoader}
}),
RouterModule.forRoot(routes)
],
providers: [
KeycloakService,
KEYCLOAK_HTTP_PROVIDER,
ResponsivenessService,
AccountServiceClient,
TranslateUtil,
ToastNotifier,
{ provide: LocationStrategy, useClass: HashLocationStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule { }

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,67 @@
<div class="page-header">
<h1>{{'editAccountHtmlTitle' | translate}}</h1>
</div>
<div class="col-sm-9 content-area">
<div class="row">
<div class="col-md-10">
</div>
<div class="col-md-2 subtitle">
<span class="subtitle"><span class="required">*</span> {{'requiredFields' | translate}}</span>
</div>
<hr/>
</div>
<form #formGroup="ngForm" (ngSubmit)="saveAccount()" class="form-horizontal">
<div class="form-group ">
<div class="col-sm-2 col-md-2">
<label for="username" class="control-label">{{'username' | translate}}</label>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="username" name="username" disabled value="{{username}}" >
</div>
</div>
<div class="form-group ">
<div class="col-sm-2 col-md-2">
<label for="email" class="control-label">{{'email' | translate }}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input type="email" class="form-control" id="email" name="email" required autofocus="" ngModel type="text">
</div>
</div>
<div class="form-group ">
<div class="col-sm-2 col-md-2">
<label for="firstName" class="control-label">{{'firstName' | translate}}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input class="form-control" id="firstName" required name="firstName" ngModel type="text">
</div>
</div>
<div class="form-group ">
<div class="col-sm-2 col-md-2">
<label for="lastName" class="control-label">{{'lastName' | translate}}</label> <span class="required">*</span>
</div>
<div class="col-sm-10 col-md-10">
<input class="form-control" id="lastName" required name="lastName" ngModel type="text">
</div>
</div>
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="submit" [disabled]="!formGroup.valid || !formGroup.dirty" class="btn btn-primary btn-lg" name="submitAction">{{'doSave' | translate}}</button>
</div>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,67 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit, ViewChild} from '@angular/core';
import {Response} from '@angular/http';
import {FormGroup} from '@angular/forms';
import {AccountServiceClient} from '../../account-service/account.service';
@Component({
selector: 'app-account-page',
templateUrl: './account-page.component.html',
styleUrls: ['./account-page.component.css']
})
export class AccountPageComponent implements OnInit {
@ViewChild('formGroup') private formGroup: FormGroup;
// using ordinary variable here
// disabled fields not working properly with FormGroup
// FormGroup.getRawValue() causes page refresh. Not sure why?
private username: string;
constructor(private accountSvc: AccountServiceClient ) {
accountSvc.doGetRequest("/", (res: Response) => this.handleGetResponse(res));
}
public saveAccount() {
console.log("posting: " + JSON.stringify(this.formGroup.value));
this.accountSvc.doPostRequest("/", (res: Response) => this.handlePostResponse(res), this.formGroup.value);
this.formGroup.reset(this.formGroup.value);
}
protected handleGetResponse(res: Response) {
const response: any = res.json();
this.username = response.username;
this.formGroup.reset(response);
console.log('**** response from account REST API ***');
console.log(JSON.stringify(res));
console.log('*** formGroup ***');
console.log(JSON.stringify(this.formGroup.value));
console.log('***************************************');
}
protected handlePostResponse(res: Response) {
console.log('**** response from account POST ***');
console.log(JSON.stringify(res));
console.log('***************************************');
}
ngOnInit() {
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Application} from './application';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export abstract class AppCard {
abstract app: Application;
abstract sessions: any[];
/*
protected getName(): string {
if (this.app.hasOwnProperty('name')) {
return this.translateUtil.translate(this.app.name);
}
return this.app.clientId;
}
protected getDescription (): string {
if (!this.app.hasOwnProperty('description')) return null;
let desc: string = this.app.description;
if (desc.indexOf("//icon") > -1) {
desc = desc.substring(0, desc.indexOf("//icon"));
}
return desc;
}*/
protected isSessionActive() : boolean {
for (let session of this.sessions) {
for (let client of session.clients) {
if (this.app.clientId === client.clientId) return true;
}
}
return false;
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {TranslateUtil} from '../../ngx-translate/translate.util';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class Application {
constructor(private app: any, private translateUtil: TranslateUtil ) {
this.setIcon();
}
private setIcon(): void {
this.app.icon = "pficon-key";
if (!this.app.hasOwnProperty('description')) {
return;
}
let desc: string = this.app.description;
const iconIndex: number = desc.indexOf("//icon=");
if (iconIndex > -1) {
this.app.icon = desc.substring(iconIndex + 7, desc.length);
}
}
public get clientId():string {
return this.app.name;
}
public get name():string {
if (this.app.hasOwnProperty('name')) {
return this.translateUtil.translate(this.app.name);
}
return this.app.clientId;
}
public get description(): string {
if (!this.app.hasOwnProperty('description')) return null;
let desc: string = this.app.description;
if (desc.indexOf("//icon") > -1) {
desc = desc.substring(0, desc.indexOf("//icon"));
}
return desc;
}
public get icon(): string {
return this.app.icon;
}
public get effectiveUrl(): string {
return this.app.effectiveUrl;
}
}

View file

@ -0,0 +1,17 @@
/*.card-pf-view .card-pf-top-element .app-icon {
display: block;
font-size: 46px;
height: 40px;
line-height: 40px;
margin: 0 auto;
text-align: center;
width: 40px;
}*/
/*
.col-lg-2 .card-pf-view .card-pf-top-element .card-pf-small-icon-circle {
font-size: 23px;
height: 54px;
line-height: 50px;
width: 54px;
}*/

View file

@ -0,0 +1,54 @@
<div class="cards-pf">
<toolbar #toolbar [filterProps]="propLabels" [sortProps]="propLabels" [actionButtons]="actionButtons"></toolbar>
<div *ngIf="toolbar.activeView === 'LargeCards'" class="container-fluid container-cards-pf">
<div class="row row-cards-pf">
<large-app-card *ngFor="let app of (applications | filterby:toolbar.filterBy.prop:toolbar.filterText | orderby:toolbar.sortBy.prop:toolbar.isSortAscending )"
class="col-xs-12 col-sm-6 col-md-4 col-lg-3"
[app]="app"
[sessions]="sessions">
</large-app-card>
</div>
</div>
<div *ngIf="toolbar.activeView === 'SmallCards'" class="container-fluid container-cards-pf">
<div class="row row-cards-pf">
<small-app-card *ngFor="let app of (applications | filterby:toolbar.filterBy.prop:toolbar.filterText | orderby:toolbar.sortBy.prop:toolbar.isSortAscending )"
class="col-xs-12 col-sm-3 col-md-2"
[app]="app"
[sessions]="sessions">
</small-app-card>
</div>
</div>
<div *ngIf="toolbar.activeView === 'List'" class="container-fluid">
<div class="list-group list-view-pf list-view-pf-view">
<row-app-card *ngFor="let app of (applications | filterby:toolbar.filterBy.prop:toolbar.filterText | orderby:toolbar.sortBy.prop:toolbar.isSortAscending )"
class="list-group-item"
[app]="app"
[sessions]="sessions">
</row-app-card>
</div>
</div>
</div>
<script>
$(function () {
// matchHeight the contents of each .card-pf and then the .card-pf itself
$(".row-cards-pf > [class*='col'] > .card-pf > .card-pf-body").matchHeight();
});
$(document).ready(function () {
// Card Single Select
$('.card-pf-view-single-select').click(function () {
if ($(this).hasClass('active'))
{
$(this).removeClass('active');
} else
{
$('.card-pf-view-single-select').removeClass('active');
$(this).addClass('active');
}
});
});
</script>

View file

@ -0,0 +1,95 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit} from '@angular/core';
import {Response} from '@angular/http';
import {TranslateUtil} from '../../ngx-translate/translate.util';
import {AccountServiceClient} from '../../account-service/account.service';
import {Application} from './application';
import {View} from '../widgets/toolbar.component';
import {PropertyLabel} from '../widgets/property.label';
import {ActionButton} from '../widgets/action.button';
import {RefreshButton, Refreshable} from '../widgets/refresh.button';
declare const resourceUrl: string;
type SelectableProperty = "name" | "description";
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'app-applications-page',
templateUrl: 'applications-page.component.html',
styleUrls: ['applications-page.component.css']
})
export class ApplicationsPageComponent implements Refreshable, OnInit {
private activeView: View = "LargeCards";
private resourceUrl: string = resourceUrl;
private applications: Application[] = [];
private isSortAscending: boolean = true;
private sortBy: SelectableProperty = "name";
private filterBy: SelectableProperty = "name";
private filterText: string = "";
private propLabels: PropertyLabel[] = [];
private actionButtons: ActionButton[] = [];
private sessions: any[] = [];
constructor(accountSvc: AccountServiceClient, private translateUtil: TranslateUtil) {
this.initPropLabels();
this.actionButtons.push(new RefreshButton(accountSvc,"/applications", this));
accountSvc.doGetRequest("/applications", (res: Response) => this.refresh(res));
accountSvc.doGetRequest("/sessions", (res: Response) => this.handleGetSessionsResponse(res));
}
private initPropLabels(): void {
this.propLabels.push({prop: "name", label: "Name"});
this.propLabels.push({prop: "description", label: "Description"});
}
public refresh(res: Response) {
console.log('**** response from apps REST API ***');
console.log(JSON.stringify(res));
console.log('*** apps res.json() ***');
console.log(JSON.stringify(res.json().applications));
console.log('*************************************');
const newApps: Application[] = [];
for (let app of res.json().applications) {
newApps.push(new Application(app, this.translateUtil));
}
// reference must change to trigger pipes
this.applications = newApps;
}
private handleGetSessionsResponse(res: Response) {
console.log('**** response from sessions REST API ***');
console.log(JSON.stringify(res));
console.log('*** sessions res.json() ***');
console.log(JSON.stringify(res.json()));
console.log('***************************************');
this.sessions = res.json();
}
ngOnInit() {
}
}

View file

@ -0,0 +1,42 @@
<div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
<div class="card-pf-body">
<div class="card-pf-top-element">
<div class="card-pf-heading-kebab">
<div class="dropup pull-right dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropupKebabRight3" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropupKebabRight3">
<li *ngIf="app.effectiveUrl"><a href="{{app.effectiveUrl}}">Go to Application</a></li>
<li><a href="#">Revoke Grant</a></li>
<li><a href="#">Logout Session(s)</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
</div>
<div class="text-center">
<span *ngIf="app.icon" class="fa {{app.icon}} card-pf-icon-circle"></span>
</div>
</div>
<h2 class="card-pf-title text-center">
{{app.name}}
</h2>
<h4 *ngIf="app.description" class="text-center">
{{app.description}}
</h4>
<div *ngIf="isSessionActive()" class="card-pf-items text-center">
<div class="card-pf-item">
<span class="pficon pficon-screen"></span>
<span class="card-pf-item-text">Session Active</span>
<span class="fa fa-check"></span>
</div>
</div>
<p class="card-pf-info text-center"><strong>Created On</strong> 2015-03-01 02:00 AM <br/> Never Expires</p>
</div>
<div class="card-pf-view-checkbox">
<input type="checkbox">
</div>
</div>

View file

@ -0,0 +1,36 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {AppCard} from './app-card';
import {Application} from './application';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'large-app-card',
templateUrl: 'large-app-card.component.html',
styleUrls: ['large-app-card.component.css']
})
export class LargeAppCardComponent extends AppCard {
@Input() app: Application;
@Input() sessions: any[];
}

View file

@ -0,0 +1,52 @@
<div class="list-view-pf-actions">
<button class="btn btn-default">Action</button>
<div class="dropdown pull-right dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropdownKebabRight9" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownKebabRight9">
<li *ngIf="app.effectiveUrl"><a href="{{app.effectiveUrl}}">Go to Application</a></li>
<li><a href="#">Revoke Grant</a></li>
<li><a href="#">Logout Session(s)</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
</div>
<div class="list-view-pf-main-info">
<div class="list-view-pf-left">
<span class="fa {{app.icon}} list-view-pf-icon-sm"></span>
<!--<img *ngIf="app.iconSrc" type="image/svg+xml" src="{{app.iconSrc}}" alt="" width="auto" height="40px"/>
<span *ngIf="app.icon" class="fa {{app.icon}} list-view-pf-icon-sm"></span>-->
</div>
<div class="list-view-pf-body">
<div class="list-view-pf-description">
<div class="list-group-item-heading">
{{app.name}}
</div>
<div class="list-group-item-text">
{{app.description}}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>8</strong> Hosts
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>6</strong> Clusters
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-container-node"></span>
<strong>10</strong> Nodes
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-image"></span>
<strong>8</strong> Images
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,36 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {AppCard} from './app-card';
import {Application} from './application';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'row-app-card',
templateUrl: 'row-app-card.component.html',
styleUrls: ['row-app-card.component.css']
})
export class RowAppCardComponent extends AppCard {
@Input() app: Application;
@Input() sessions: any[];
}

View file

@ -0,0 +1,41 @@
<div class="card-pf card-pf-view card-pf-view-xs">
<div class="card-pf-body">
<div class="card-pf-heading-kebab">
<div class="dropdown pull-right dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropupKebabRight2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropupKebabRight2">
<li *ngIf="app.effectiveUrl"><a href="{{app.effectiveUrl}}">Go to Application</a></li>
<li><a href="#">Revoke Grant</a></li>
<li><a href="#">Logout Session(s)</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
<h2 class="card-pf-title">
<span class="pficon {{app.icon}}"></span> {{app.name}}
</h2>
</div>
<div class="progress-pf-legend">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="width: 25%;" data-toggle="tooltip" title="25% Used">
<span class="sr-only">25% Used</span>
</div>
<div class="progress-bar progress-bar-remaining" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width: 75%;" data-toggle="tooltip" title="75% Available">
<span class="sr-only">75% Available</span>
</div>
</div>
<script>
// Initialize Tooltip
jQuery(document).ready(function () {
jQuery('[data-toggle="tooltip"]').tooltip();
});
</script>
<p><span class="pficon pficon-warning-triangle-o"></span> <strong>10%</strong> in use</p>
</div>
</div>
</div>

View file

@ -0,0 +1,36 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {AppCard} from './app-card';
import {Application} from './application';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'small-app-card',
templateUrl: 'small-app-card.component.html',
styleUrls: ['small-app-card.component.css']
})
export class SmallAppCardComponent extends AppCard {
@Input() app: Application;
@Input() sessions: any[];
}

View file

@ -0,0 +1,46 @@
<div class="page-header">
<h1>Authenticator</h1>
</div>
<div class="col-sm-9 content-area">
<ol>
<li>
<p>Install <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> or Google Authenticator on your device. Both applications are available in <a href="https://play.google.com">Google Play</a> and Apple App Store.</p>
</li>
<li>
<p>Open the application and scan the barcode or enter the key.</p>
<p><img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACiElEQVR42u2ZTa6jMBCEG7FgyRF8k+RilkDiYuEmHIGlF4ieqm7+3sxodqP2IiwQ+Eskx+4qlx3Rf16bfPmX/1++ikijYyovGbSb0dp9JKMRr1XwQXVesxTpVUuj09LpmpOBKngW9HoUeeO18Kazbkmamvg6LBhVEcUtyUtyZRz9L69eF3n1Y+L8V8Rt/nH7eH1ipJs/6yOQizdgkB+33/QVyA+nwaja+Mqw/M2f4jj6jwlv1eoTv6RVOFG7sByq4KqL7uSzTtrxl+BjqNn9Gt9Yjg7Dv/G0e33CHyn3e3yDORp8/imiwk/iScSWmyr4lmx96fZ+dH+EieM789n/YO59bc1/3B9Zp0net//E8izef1hjMn+0mu2u+gjn8GprxVN50x8//fTQfzAn+vRMDVCNCIoA84/rqo9gvrYL5lo8H9J6fP4f/hPMj/WvHFWZbc3+4e+xPLvXCEfV/YdKv/wnnLdsHZNaPrQkhuiwFKmEA8F1JpUjHyrXZ3E7r4LDa6CfZK22P7EiMBFVwuVwHexPjpU63/4dzkczRM/XZkcmHZ0r4e7akqzXDLFqcpf3vb6Ecgqa/Yeqt+SvpqlqONY6zj93xdf8D/rMZ7Ec14e7TguJth3YoXQ7bqiBr9ywgxemfjxtrARcl39HcztaQHSgicuRrwfuCbQS7hs6UxKPPjj/limGOriVpnrgZ4gQyl26+ZFfI7ldzF+CTYCwKn18X30d/MxfQBNRy+iQU/fjfDWS8/zIzgfPXae1PfQTzO18MHv+x61VPxO566MGPqilVtM3pb081ucaOHq9n/mLi8xz/Q7mx/mlh8Tm1Pytn2hu+uH5KqO12PkClFQarYN//7/68mr5LwnkYjOKw3BlAAAAAElFTkSuQmCC" alt="Figure: Barcode"></p>
<p><span class="code">MVFU 4UKB IYZU 2RBZ NJSW 2MTK NQZT QVBZ</span></p>
</li>
<li>
<p>Enter the one-time code provided by the application and click Save to finish the setup.</p>
</li>
</ol>
<hr>
<form action="http://localhost:8180/auth/realms/master/account/totp" class="form-horizontal" method="post">
<input id="stateChecker" name="stateChecker" value="7L6A5K0Mghuc4cm2DBF78rIMI5140AnKc01_q3Pj-4o" type="hidden">
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="totp" class="control-label">One-time code</label>
</div>
<div class="col-sm-10 col-md-10">
<input class="form-control" id="totp" name="totp" autocomplete="off" autofocus="" type="text">
<input id="totpSecret" name="totpSecret" value="eKNQAF3MD9jem2jl38T9" type="hidden">
</div>
</div>
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="submit" class="btn btn-primary btn-lg" name="submitAction" value="Save">Save</button>
<button type="submit" class="btn btn-default btn-lg" name="submitAction" value="Cancel">Cancel</button>
</div>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,32 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-authenticator-page',
templateUrl: './authenticator-page.component.html',
styleUrls: ['./authenticator-page.component.css']
})
export class AuthenticatorPageComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-page-not-found',
templateUrl: './page-not-found.component.html',
styleUrls: ['./page-not-found.component.css']
})
export class PageNotFoundComponent implements OnInit {
constructor() {
}
ngOnInit() {
}
}

View file

@ -0,0 +1,55 @@
<div class="page-header">
<h1>{{'changePasswordHtmlTitle' | translate}}</h1>
</div>
<div class="row">
<div class="col-md-10">
</div>
<div class="col-md-2 subtitle">
<span class="subtitle">{{'allFieldsRequired' | translate}}</span>
</div>
<hr/>
</div>
<form #formGroup="ngForm" (ngSubmit)="changePassword()" class="form-horizontal">
<input readonly="" value="this is not a login form" style="display: none;" type="text">
<input readonly="" value="this is not a login form" style="display: none;" type="password">
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="password" class="control-label">{{'password' | translate}}</label>
</div>
<div class="col-sm-10 col-md-10">
<input ngModel class="form-control" id="password" name="password" autofocus="" autocomplete="off" type="password">
</div>
</div>
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="password-new" class="control-label">{{'passwordNew' | translate}}</label>
</div>
<div class="col-sm-10 col-md-10">
<input ngModel class="form-control" id="newPassword" name="newPassword" autocomplete="off" type="password">
</div>
</div>
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="password-confirm" class="control-label">{{'passwordConfirm' | translate}}</label>
</div>
<div class="col-sm-10 col-md-10">
<input ngModel class="form-control" id="confirmation" name="confirmation" autocomplete="off" type="password">
</div>
</div>
<div class="form-group">
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="submit" class="btn btn-primary btn-lg" name="submitAction">{{'doSave' | translate}}</button>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,48 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit, ViewChild} from '@angular/core';
import {Response} from '@angular/http';
import {FormGroup} from '@angular/forms';
import {AccountServiceClient} from '../../account-service/account.service';
@Component({
selector: 'app-password-page',
templateUrl: './password-page.component.html',
styleUrls: ['./password-page.component.css']
})
export class PasswordPageComponent implements OnInit {
@ViewChild('formGroup') private formGroup: FormGroup;
constructor(private accountSvc: AccountServiceClient) {
}
public changePassword() {
console.log("posting: " + JSON.stringify(this.formGroup.value));
this.accountSvc.doPostRequest("/credentials", (res: Response) => this.handlePostResponse(res), this.formGroup.value);
}
protected handlePostResponse(res: Response) {
console.log('**** response from account POST ***');
console.log(JSON.stringify(res));
console.log('***************************************');
}
ngOnInit() {
}
}

View file

@ -0,0 +1,27 @@
<div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
<div class="card-pf-body">
<div class="card-pf-top-element">
<div class="card-pf-heading-kebab">
<div class="dropup pull-right dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropupKebabRight3" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropupKebabRight3">
<li><a href="#">Logout Session</a></li>
<li><a href="#">View Clients</a></li>
</ul>
</div>
</div>
<div class="text-center">
<span class="fa fa-clock-o card-pf-icon-circle"></span>
</div>
</div>
<h2 class="card-pf-title text-center">
<strong>{{'ip' | translate}}</strong> {{session.ipAddress}}
</h2>
<p class="card-pf-info text-center"><strong>{{'started' | translate}}</strong> {{session.started * 1000 | date:'medium'}}</p>
<p class="card-pf-info text-center"><strong>{{'lastAccess' | translate}}</strong> {{session.lastAccess * 1000 | date:'medium'}}</p>
<p class="card-pf-info text-center"><strong>{{'expires' | translate}}</strong> {{session.expires * 1000 | date:'medium'}}</p>
</div>
</div>

View file

@ -0,0 +1,34 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {Session} from './session';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'large-session-card',
templateUrl: 'large-session-card.component.html',
styleUrls: ['large-session-card.component.css']
})
export class LargeSessionCardComponent {
@Input() session: Session;
}

View file

@ -0,0 +1,32 @@
<div class="col-sm-9 content-area">
<table class="table table-striped table-bordered">
<thead>
<tr>
<td>{{'ip' | translate}}</td>
<td>{{'started' | translate}}</td>
<td>{{'lastAccess' | translate}}</td>
<td>{{'expires' | translate}}</td>
<td>{{'clients' | translate}}</td>
</tr>
</thead>
<tbody *ngFor="let session of sessions">
<tr>
<td>{{session.ipAddress}}</td>
<td>{{session.started * 1000 | date:'medium'}}</td>
<td>{{session.lastAccess * 1000 | date: 'medium'}}</td>
<td>{{session.expires * 1000 | date: 'medium'}}</td>
<td>
<span *ngFor="let client of session.clients">
{{client.clientId}}<br>
</span>
</td>
</tr>
</tbody>
</table>
<a (click)="logoutAllSessions()" id="logout-all-sessions" href="#/sessions">{{'doLogOutAllSessions' | translate}}</a>
</div>

View file

@ -0,0 +1,35 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {Session} from './session';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'row-session-card',
templateUrl: 'row-session-card.component.html',
styleUrls: ['row-session-card.component.css']
})
export class RowSessionCardComponent {
@Input() sessions: Session[];
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class Session {
constructor(private session: any) {}
get ipAddress(): string {
return this.session.ipAddress;
}
get started(): number {
return this.session.started;
}
get lastAccess(): number {
return this.session.lastAccess;
}
get expires(): number {
return this.session.expires;
}
get clients(): string[] {
return this.session.clients;
}
}

View file

@ -0,0 +1,60 @@
<div class="cards-pf">
<toolbar #toolbar [filterProps]="filterLabels" [sortProps]="sortLabels" [actionButtons]="actionButtons"></toolbar>
<div *ngIf="toolbar.activeView === 'LargeCards'" class="container-fluid container-cards-pf">
<div class="row row-cards-pf">
<large-session-card *ngFor="let session of (sessions | filterby:toolbar.filterBy.prop:toolbar.filterText | orderby:toolbar.sortBy.prop:toolbar.isSortAscending )"
class="col-xs-12 col-sm-6 col-md-4 col-lg-3"
[session]="session">
</large-session-card>
</div>
</div>
<div *ngIf="toolbar.activeView === 'SmallCards'" class="container-fluid container-cards-pf">
<div class="row row-cards-pf">
<small-session-card *ngFor="let session of (sessions | filterby:toolbar.filterBy.prop:toolbar.filterText | orderby:toolbar.sortBy.prop:toolbar.isSortAscending )"
class="col-xs-12 col-sm-3 col-md-2"
[session]="session">
</small-session-card>
</div>
</div>
<div *ngIf="toolbar.activeView === 'List'" class="container-fluid">
<div class="page-header">
<h1>{{'sessionsHtmlTitle' | translate}}</h1>
</div>
<div class="col-sm-9 content-area">
<table class="table table-striped table-bordered">
<thead>
<tr>
<td>{{'ip' | translate}}</td>
<td>{{'started' | translate}}</td>
<td>{{'lastAccess' | translate}}</td>
<td>{{'expires' | translate}}</td>
<td>{{'clients' | translate}}</td>
</tr>
</thead>
<tbody *ngFor="let item of response">
<tr>
<td>{{item.ipAddress}}</td>
<td>{{item.started * 1000 | date:'medium'}}</td>
<td>{{item.lastAccess * 1000 | date: 'medium'}}</td>
<td>{{item.expires * 1000 | date: 'medium'}}</td>
<td>
<span *ngFor="let client of item.clients">
{{client.clientId}}<br>
</span>
</td>
</tr>
</tbody>
</table>
<a (click)="logoutAllSessions()" id="logout-all-sessions" href="#/sessions">{{'doLogOutAllSessions' | translate}}</a>
</div>
</div>
</div>

View file

@ -0,0 +1,115 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit} from '@angular/core';
import {Response} from '@angular/http';
import {AccountServiceClient} from '../../account-service/account.service';
import {TranslateUtil} from '../../ngx-translate/translate.util';
import {View} from '../widgets/toolbar.component';
import {PropertyLabel} from '../widgets/property.label';
import {ActionButton} from '../widgets/action.button';
import {RefreshButton, Refreshable} from '../widgets/refresh.button';
import {Session} from './session';
@Component({
selector: 'app-sessions-page',
templateUrl: './sessions-page.component.html',
styleUrls: ['./sessions-page.component.css']
})
export class SessionsPageComponent implements Refreshable, OnInit {
private filterLabels: PropertyLabel[] = [];
private sortLabels: PropertyLabel[] = [];
private response: any[] = [];
private sessions: Session[] = [];
private actionButtons: ActionButton[] = [];
constructor(private accountSvc: AccountServiceClient, private translateUtil: TranslateUtil ) {
this.initPropLabels();
this.actionButtons.push(new LogoutAllButton(accountSvc, translateUtil));
this.actionButtons.push(new RefreshButton(accountSvc,"/sessions", this));
accountSvc.doGetRequest("/sessions", (res: Response) => this.refresh(res));
}
private initPropLabels(): void {
this.filterLabels.push({prop: "ipAddress", label: "IP"});
this.sortLabels.push({prop: "ipAddress", label: "IP"});
this.sortLabels.push({prop: "started", label: "Started"});
this.sortLabels.push({prop: "lastAccess", label: "Last Access"});
this.sortLabels.push({prop: "expires", label: "Expires"});
}
public refresh(res: Response) {
console.log('**** response from account REST API ***');
console.log(JSON.stringify(res));
console.log('*** res.json() ***');
console.log(JSON.stringify(res.json()));
console.log('***************************************');
this.response = res.json();
const newSessions: Session[] = [];
for (let session of res.json()) {
newSessions.push(new Session(session));
}
// reference must change to trigger pipes
this.sessions = newSessions;
}
private logoutAllSessions() {
this.accountSvc.doDelete("/sessions",
(res: Response) => this.handleLogoutResponse(res),
{params: {current: true}},
"Logging out all sessions.");
}
private handleLogoutResponse(res: Response) {
console.log('**** response from account DELETE ***');
console.log(JSON.stringify(res));
console.log('***************************************');
}
ngOnInit() {
}
}
class LogoutAllButton implements ActionButton {
public readonly label: string = "Logout All"; //TODO: localize in constructor
public readonly tooltip: string;
constructor(private accountSvc: AccountServiceClient, translateUtil: TranslateUtil ) {
this.tooltip = translateUtil.translate('doLogOutAllSessions');
}
performAction(): void {
this.accountSvc.doDelete("/sessions",
(res: Response) => this.handleLogoutResponse(res),
{params: {current: true}},
"Logging out all sessions.");
}
private handleLogoutResponse(res: Response) {
console.log('**** response from account DELETE ***');
console.log(JSON.stringify(res));
console.log('***************************************');
}
}

View file

@ -0,0 +1,22 @@
<div class="card-pf card-pf-view card-pf-view-xs">
<div class="card-pf-body">
<div class="card-pf-heading-kebab">
<div class="dropdown pull-right dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropupKebabRight2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropupKebabRight2">
<li><a href="#">Logout Session</a></li>
<li><a href="#">View Clients</a></li>
</ul>
</div>
<h2 class="card-pf-title">
<span class="fa fa-clock-o"></span> {{session.ipAddress}}
</h2>
<p class="card-pf-info text-center"><strong>{{'started' | translate}}</strong> {{session.started * 1000 | date:'shortTime'}}</p>
<p class="card-pf-info text-center"><strong>{{'lastAccess' | translate}}</strong> {{session.lastAccess * 1000 | date:'shortTime'}}</p>
<p class="card-pf-info text-center"><strong>{{'expires' | translate}}</strong> {{session.expires * 1000 | date:'shortTime'}}</p>
</div>
</div>
</div>

View file

@ -0,0 +1,34 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input} from '@angular/core';
import {Session} from './session';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'small-session-card',
templateUrl: 'small-session-card.component.html',
styleUrls: ['small-session-card.component.css']
})
export class SmallSessionCardComponent {
@Input() session: Session;
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Icon} from '../../page/icon';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export interface ActionButton {
readonly label: string | Icon;
readonly tooptip?: string;
performAction(): void;
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
* Property name and its human-readable, localized display text.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export interface PropertyLabel {
readonly prop: string;
readonly label: string;
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Response} from '@angular/http';
import {ActionButton} from './action.button';
import {Icon} from '../../page/icon';
import {AccountServiceClient} from '../../account-service/account.service';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class RefreshButton implements ActionButton {
readonly label:Icon = new Icon('fa', 'refresh');
readonly tooltip:string = 'Refresh'; //TODO: localize in constructor
constructor(private accountSvc: AccountServiceClient,
private request: string,
private refreshable:Refreshable) {}
public performAction(): void {
this.accountSvc.doGetRequest(this.request, (res: Response) => {
this.refreshable.refresh(res);
});
}
}
export interface Refreshable {
refresh(response:Response): void;
}

View file

@ -0,0 +1,100 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Component, Input, OnInit} from '@angular/core';
import {PropertyLabel} from './property.label';
import {ActionButton} from './action.button';
import {Icon} from '../../page/icon';
export type View = "LargeCards" | "SmallCards" | "List";
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Component({
moduleId: module.id, // need this for styleUrls path to work properly with Systemjs
selector: 'toolbar',
templateUrl: 'toolbar.html',
styleUrls: ['toolbar.css']
})
export class ToolbarComponent implements OnInit {
@Input() filterProps: PropertyLabel[];
@Input() sortProps: PropertyLabel[];
@Input() actionButtons: ActionButton[];
// TODO: localize in constructor
readonly sortByTooltip: string = "Sort by...";
readonly sortAscendingTooltip: string = "Sort Ascending";
readonly sortDescendingTooltip: string = "Sort Descending";
private isSortAscending: boolean = true;
private sortBy: PropertyLabel;
private filterBy: PropertyLabel;
private filterText: string = "";
public activeView: View = "LargeCards";
ngOnInit() {
if (this.filterProps && this.filterProps.length > 0) {
this.filterBy = this.filterProps[0];
}
if (this.sortProps && this.sortProps.length > 0) {
this.sortBy = this.sortProps[0];
}
}
private changeView(activeView: View) {
this.activeView = activeView;
}
private toggleSort() {
this.isSortAscending = !this.isSortAscending;
}
private changeSortByProp(prop: PropertyLabel) {
this.sortBy = prop;
}
private changeFilterByProp(prop: PropertyLabel) {
this.filterBy = prop;
this.filterText = "";
}
private selectedFilterClass(prop: PropertyLabel): string {
if (this.filterBy === prop) {
return "selected";
} else {
return "";
}
}
private selectedSortByClass(prop: PropertyLabel): string {
if (this.sortBy === prop) {
return "selected";
} else {
return "";
}
}
isIconButton(button: ActionButton): boolean {
return button.label instanceof Icon;
}
}

View file

@ -0,0 +1,137 @@
<div class="container-fluid">
<div class="row toolbar-pf">
<div class="col-sm-12">
<div class="toolbar-pf-actions">
<div *ngIf="filterProps" class="form-group toolbar-pf-filter">
<label class="sr-only" for="filter">{{filterBy.label}}</label>
<div class="input-group">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{filterBy.label}} <span class="caret"></span></button>
<ul class="dropdown-menu">
<li *ngFor="let prop of filterProps" (click)="changeFilterByProp(prop)" class="{{selectedFilterClass(prop)}}"><a>{{prop.label}}</a></li>
</ul>
</div><!-- /btn-group -->
<form #filterForm="ngForm">
<input [(ngModel)]="filterText" name="filterText" type="text" class="form-control" id="filter" placeholder="Filter By {{filterBy.label}}...">
</form>
</div><!-- /input-group -->
</div>
<div *ngIf="sortProps" class="form-group">
<div class="dropdown btn-group">
<button type="button"
class="btn btn-default dropdown-toggle"
data-toggle="tooltip"
data-toggle="dropdown"
data-placement="top"
title="{{sortByTooltip}}"
aria-haspopup="true"
aria-expanded="false">{{sortBy.label}} <span class="caret"></span></button>
<ul class="dropdown-menu">
<li *ngFor="let prop of sortProps" (click)="changeSortByProp(prop)" class="{{selectedSortByClass(prop)}}"><a>{{prop.label}}</a></li>
</ul>
</div>
<button (click)="toggleSort()" class="btn btn-link" type="button">
<span *ngIf="!isSortAscending"
class="fa fa-sort-alpha-asc"
data-toggle="toltip"
data-placement="top"
title="{{sortAscendingTooltip}}"></span>
<span *ngIf="isSortAscending"
class="fa fa-sort-alpha-desc"
data-toggle="toltip"
data-placement="top"
title="{{sortDescendingTooltip}}"></span>
</button>
</div>
<div *ngIf="actionButtons">
<span class="form-group" *ngFor="let action of actionButtons">
<button *ngIf="!isIconButton(action)"
(click)="action.performAction()"
data-toggle="tooltip"
data-placement="top"
title="{{action.tooltip}}"
class="btn btn-default"
type="button">{{action.label}}</button>
<button *ngIf="isIconButton(action)"
(click)="action.performAction()"
data-toggle="tooltip"
data-placement="top"
title="{{action.tooltip}}"
class="btn btn-link"
type="button">
<span class="{{action.label.getClasses()}}"></span>
</button>
</span>
<!--<button class="btn btn-default" type="button">Action</button>
<div class="dropdown btn-group dropdown-kebab-pf">
<button class="btn btn-link dropdown-toggle" type="button" id="dropdownKebab" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="fa fa-ellipsis-v"></span>
</button>
<ul class="dropdown-menu " aria-labelledby="dropdownKebab">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>-->
</div>
<div class="toolbar-pf-action-right">
<!--<div class="form-group toolbar-pf-find">
<button class="btn btn-link btn-find" type="button">
<span class="fa fa-search"></span>
</button>
<div class="find-pf-dropdown-container">
<input type="text" class="form-control" id="find" placeholder="Find By Keyword...">
<div class="find-pf-buttons">
<span class="find-pf-nums">1 of 3</span>
<button class="btn btn-link" type="button">
<span class="fa fa-angle-up"></span>
</button>
<button class="btn btn-link" type="button">
<span class="fa fa-angle-down"></span>
</button>
<button class="btn btn-link btn-find-close" type="button">
<span class="pficon pficon-close"></span>
</button>
</div>
</div>
</div>-->
<div class="form-group toolbar-pf-view-selector">
<button (click)="changeView('LargeCards')" class="btn btn-link "><i class="fa fa-th-large"></i></button>
<button (click)="changeView('SmallCards')" class="btn btn-link "><i class="fa fa-th"></i></button>
<button (click)="changeView('List')" class="btn btn-link "><i class="fa fa-th-list"></i></button>
</div>
</div>
</div>
<!--<div class="row toolbar-pf-results">
<div class="col-sm-12">
<h5>40 Results</h5>
<p>Active filters:</p>
<ul class="list-inline">
<li>
<span class="label label-info">
Name: nameofthething
<a href="#"><span class="pficon pficon-close"></span></a>
</span>
</li>
<li>
<span class="label label-info">
Name: nameofthething
<a href="#"><span class="pficon pficon-close"></span></a>
</span>
</li>
<li>
<span class="label label-info">
Name: nameofthething
<a href="#"><span class="pficon pficon-close"></span></a>
</span>
</li>
</ul>
<p><a href="#">Clear All Filters</a></p>
</div><!-- /col --
</div>--><!-- /row -->
</div><!-- /col -->
</div><!-- /row -->
</div><!-- /container -->

View file

@ -0,0 +1,478 @@
/*
* MIT License
*
* Copyright 2017 Andy Hanson
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export as namespace Keycloak;
export = Keycloak;
/**
* Creates a new Keycloak client instance.
* @param config Path to a JSON config file or a plain config object.
*/
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
declare namespace Keycloak {
type KeycloakAdapterName = 'cordova'|'default';
type KeycloakOnLoad = 'login-required'|'check-sso';
type KeycloakResponseMode = 'query'|'fragment';
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
interface KeycloakInitOptions {
/**
* @private Undocumented.
*/
adapter?: KeycloakAdapterName;
/**
* Specifies an action to do on load.
*/
onLoad?: KeycloakOnLoad;
/**
* Set an initial value for the token.
*/
token?: string;
/**
* Set an initial value for the refresh token.
*/
refreshToken?: string;
/**
* Set an initial value for the id token (only together with `token` or
* `refreshToken`).
*/
idToken?: string;
/**
* Set an initial value for skew between local time and Keycloak server in
* seconds (only together with `token` or `refreshToken`).
*/
timeSkew?: number;
/**
* Set to enable/disable monitoring login state.
* @default true
*/
checkLoginIframe?: boolean;
/**
* Set the interval to check login state (in seconds).
* @default 5
*/
checkLoginIframeInterval?: boolean;
/**
* Set the OpenID Connect response mode to send to Keycloak upon login.
* @default fragment After successful authentication Keycloak will redirect
* to JavaScript application with OpenID Connect parameters
* added in URL fragment. This is generally safer and
* recommended over query.
*/
responseMode?: KeycloakResponseMode;
/**
* Set the OpenID Connect flow.
* @default standard
*/
flow?: KeycloakFlow;
}
interface KeycloakLoginOptions {
/**
* @private Undocumented.
*/
scope?: string;
/**
* Specifies the uri to redirect to after login.
*/
redirectUri?: string;
/**
* By default the login screen is displayed if the user is not logged into
* Keycloak. To only authenticate to the application if the user is already
* logged in and not display the login page if the user is not logged in, set
* this option to `'none'`. To always require re-authentication and ignore
* SSO, set this option to `'login'`.
*/
prompt?: 'none'|'login';
/**
* If value is `'register'` then user is redirected to registration page,
* otherwise to login page.
*/
action?: 'register';
/**
* Used just if user is already authenticated. Specifies maximum time since
* the authentication of user happened. If user is already authenticated for
* longer time than `'maxAge'`, the SSO is ignored and he will need to
* authenticate again.
*/
maxAge?: number;
/**
* Used to pre-fill the username/email field on the login form.
*/
loginHint?: string;
/**
* Used to tell Keycloak which IDP the user wants to authenticate with.
*/
idpHint?: string;
/**
* Specifies the desired locale for the UI.
*/
locale?: string;
}
type KeycloakPromiseCallback<T> = (result: T) => void;
interface KeycloakPromise<TSuccess, TError> {
/**
* Function to call if the promised action succeeds.
*/
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
/**
* Function to call if the promised action throws an error.
*/
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
}
interface KeycloakError {
error: string;
error_description: string;
}
interface KeycloakAdapter {
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
logout(options?: any): KeycloakPromise<void, void>;
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
accountManagement(): KeycloakPromise<void, void>;
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
}
interface KeycloakProfile {
id?: string;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
enabled?: boolean;
emailVerified?: boolean;
totp?: boolean;
createdTimestamp?: number;
}
// export interface KeycloakUserInfo {}
/**
* A client for the Keycloak authentication server.
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
*/
interface KeycloakInstance {
/**
* Is true if the user is authenticated, false otherwise.
*/
authenticated?: boolean;
/**
* The user id.
*/
subject?: string;
/**
* Response mode passed in init (default value is `'fragment'`).
*/
responseMode?: KeycloakResponseMode;
/**
* Response type sent to Keycloak with login requests. This is determined
* based on the flow value used during initialization, but can be overridden
* by setting this value.
*/
responseType?: KeycloakResponseType;
/**
* Flow passed in init.
*/
flow?: KeycloakFlow;
/**
* The realm roles associated with the token.
*/
realmAccess?: { roles: string[] };
/**
* The resource roles associated with the token.
*/
resourceAccess?: string[];
/**
* The base64 encoded token that can be sent in the Authorization header in
* requests to services.
*/
token?: string;
/**
* The parsed token as a JavaScript object.
*/
tokenParsed?: {
exp?: number;
iat?: number;
nonce?: string;
sub?: string;
session_state?: string;
realm_access?: { roles: string[] };
resource_access?: string[];
};
/**
* The base64 encoded refresh token that can be used to retrieve a new token.
*/
refreshToken?: string;
/**
* The parsed refresh token as a JavaScript object.
*/
refreshTokenParsed?: { nonce?: string };
/**
* The base64 encoded ID token.
*/
idToken?: string;
/**
* The parsed id token as a JavaScript object.
*/
idTokenParsed?: { nonce?: string };
/**
* The estimated time difference between the browser time and the Keycloak
* server in seconds. This value is just an estimation, but is accurate
* enough when determining if a token is expired or not.
*/
timeSkew?: number;
/**
* @private Undocumented.
*/
loginRequired?: boolean;
/**
* @private Undocumented.
*/
authServerUrl?: string;
/**
* @private Undocumented.
*/
realm?: string;
/**
* @private Undocumented.
*/
clientId?: string;
/**
* @private Undocumented.
*/
clientSecret?: string;
/**
* @private Undocumented.
*/
redirectUri?: string;
/**
* @private Undocumented.
*/
sessionId?: string;
/**
* @private Undocumented.
*/
profile?: KeycloakProfile;
/**
* @private Undocumented.
*/
userInfo?: {}; // KeycloakUserInfo;
/**
* Called when the adapter is initialized.
*/
onReady?(authenticated?: boolean): void;
/**
* Called when a user is successfully authenticated.
*/
onAuthSuccess?(): void;
/**
* Called if there was an error during authentication.
*/
onAuthError?(errorData: KeycloakError): void;
/**
* Called when the token is refreshed.
*/
onAuthRefreshSuccess?(): void;
/**
* Called if there was an error while trying to refresh the token.
*/
onAuthRefreshError?(): void;
/**
* Called if the user is logged out (will only be called if the session
* status iframe is enabled, or in Cordova mode).
*/
onAuthLogout?(): void;
/**
* Called when the access token is expired. If a refresh token is available
* the token can be refreshed with Keycloak#updateToken, or in cases where
* it's not (ie. with implicit flow) you can redirect to login screen to
* obtain a new access token.
*/
onTokenExpired?(): void;
/**
* Called to initialize the adapter.
* @param initOptions Initialization options.
* @returns A promise to set functions to be invoked on success or error.
*/
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
/**
* Redirects to login form.
* @param options Login options.
*/
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
/**
* Redirects to logout.
* @param options Logout options.
* @param options.redirectUri Specifies the uri to redirect to after logout.
*/
logout(options?: any): KeycloakPromise<void, void>;
/**
* Redirects to registration form.
* @param options Supports same options as Keycloak#login but `action` is
* set to `'register'`.
*/
register(options?: any): KeycloakPromise<void, void>;
/**
* Redirects to the Account Management Console.
*/
accountManagement(): KeycloakPromise<void, void>;
/**
* Returns the URL to login form.
* @param options Supports same options as Keycloak#login.
*/
createLoginUrl(options?: KeycloakLoginOptions): string;
/**
* Returns the URL to logout the user.
* @param options Logout options.
* @param options.redirectUri Specifies the uri to redirect to after logout.
*/
createLogoutUrl(options?: any): string;
/**
* Returns the URL to registration page.
* @param options Supports same options as Keycloak#createLoginUrl but
* `action` is set to `'register'`.
*/
createRegisterUrl(options?: KeycloakLoginOptions): string;
/**
* Returns the URL to the Account Management Console.
*/
createAccountUrl(): string;
/**
* Returns true if the token has less than `minValidity` seconds left before
* it expires.
* @param minValidity If not specified, `0` is used.
*/
isTokenExpired(minValidity?: number): boolean;
/**
* If the token expires within `minValidity` seconds, the token is refreshed.
* If the session status iframe is enabled, the session status is also
* checked.
* @returns A promise to set functions that can be invoked if the token is
* still valid, or if the token is no longer valid.
* @example
* ```js
* keycloak.updateToken(5).success(function(refreshed) {
* if (refreshed) {
* alert('Token was successfully refreshed');
* } else {
* alert('Token is still valid');
* }
* }).error(function() {
* alert('Failed to refresh the token, or the session has expired');
* });
*/
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
/**
* Clears authentication state, including tokens. This can be useful if
* the application has detected the session was expired, for example if
* updating token fails. Invoking this results in Keycloak#onAuthLogout
* callback listener being invoked.
*/
clearToken(): void;
/**
* Returns true if the token has the given realm role.
* @param role A realm role name.
*/
hasRealmRole(role: string): boolean;
/**
* Returns true if the token has the given role for the resource.
* @param role A role name.
* @param resource If not specified, `clientId` is used.
*/
hasResourceRole(role: string, resource?: string): boolean;
/**
* Loads the user's profile.
* @returns A promise to set functions to be invoked on success or error.
*/
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
/**
* @private Undocumented.
*/
loadUserInfo(): KeycloakPromise<{}, void>;
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Injectable} from '@angular/core';
import {Http, Request, XHRBackend, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers} from '@angular/http';
import {KeycloakService} from './keycloak.service';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/map';
/**
* This provides a wrapper over the ng2 Http class that insures tokens are refreshed on each request.
*/
@Injectable()
export class KeycloakHttp extends Http {
constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions, private _keycloakService: KeycloakService) {
super(_backend, _defaultOptions);
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
if (!this._keycloakService.authenticated()) return super.request(url, options);
const tokenPromise: Promise<string> = this._keycloakService.getToken();
const tokenObservable: Observable<string> = Observable.fromPromise(tokenPromise);
if (typeof url === 'string') {
return tokenObservable.map(token => {
const authOptions = new RequestOptions({headers: new Headers({'Authorization': 'Bearer ' + token})});
return new RequestOptions().merge(options).merge(authOptions);
}).concatMap(opts => super.request(url, opts));
} else if (url instanceof Request) {
return tokenObservable.map(token => {
url.headers.set('Authorization', 'Bearer ' + token);
return url;
}).concatMap(request => super.request(request));
}
}
}
export function keycloakHttpFactory(backend: XHRBackend, defaultOptions: RequestOptions, keycloakService: KeycloakService) {
return new KeycloakHttp(backend, defaultOptions, keycloakService);
}
export const KEYCLOAK_HTTP_PROVIDER = {
provide: Http,
useFactory: keycloakHttpFactory,
deps: [XHRBackend, RequestOptions, KeycloakService]
};

View file

@ -0,0 +1,96 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Injectable} from '@angular/core';
// If using a local keycloak.js, uncomment this import. With keycloak.js fetched
// from the server, you get a compile-time warning on use of the Keycloak()
// method below. I'm not sure how to fix this, but it's certainly cleaner
// to get keycloak.js from the server.
//
import * as Keycloak from './keycloak';
type KeycloakClient = Keycloak.KeycloakInstance;
type InitOptions = Keycloak.KeycloakInitOptions;
@Injectable()
export class KeycloakService {
static keycloakAuth: KeycloakClient;
/**
* Configure and initialize the Keycloak adapter.
*
* @param configOptions Optionally, a path to keycloak.json, or an object containing
* url, realm, and clientId.
* @param adapterOptions Optional initiaization options. See javascript adapter docs
* for details.
* @returns {Promise<T>}
*/
static init(configOptions?: string|{}, initOptions?: InitOptions): Promise<any> {
KeycloakService.keycloakAuth = Keycloak(configOptions);
return new Promise((resolve, reject) => {
KeycloakService.keycloakAuth.init(initOptions)
.success(() => {
resolve();
})
.error((errorData: any) => {
reject(errorData);
});
});
}
authenticated(): boolean {
return KeycloakService.keycloakAuth.authenticated;
}
login() {
KeycloakService.keycloakAuth.login();
}
logout() {
KeycloakService.keycloakAuth.logout();
}
account() {
KeycloakService.keycloakAuth.accountManagement();
}
authServerUrl(): string {
return KeycloakService.keycloakAuth.authServerUrl;
}
realm(): string {
return KeycloakService.keycloakAuth.realm;
}
getToken(): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (KeycloakService.keycloakAuth.token) {
KeycloakService.keycloakAuth
.updateToken(5)
.success(() => {
resolve(<string>KeycloakService.keycloakAuth.token);
})
.error(() => {
reject('Failed to refresh token');
});
} else {
reject('Not loggen in');
}
});
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Observable} from 'rxjs/Observable';
import {TranslateLoader} from '@ngx-translate/core';
declare const l18n_msg: any;
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class DeclaredVarTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return Observable.of(l18n_msg);
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Injectable} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Injectable()
export class TranslateUtil {
constructor(private translator: TranslateService) {
}
public translate(key: string) : string {
// remove Freemarker syntax
if (key.startsWith('${') && key.endsWith('}')) {
key = key.substring(2, key.length - 1);
}
return this.translator.instant(key);
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type Vendor = "fa" | "pficon";
export class Icon {
constructor(public vendor: Vendor, public name: string) {
}
getClasses(): string {
return `${this.vendor} ${this.vendor}-${this.name}`;
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {TranslateUtil} from '../ngx-translate/translate.util';
declare const referrer: string;
declare const referrer_uri: string;
/**
* Encapsulate referrer logic.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class Referrer {
constructor(private translateUtil: TranslateUtil) {}
public exists(): boolean {
return typeof referrer !== "undefined";
}
// return a value suitable for parameterized use with ngx-translate
// example {{'backTo' | translate:referrer.getName()}}
public getName(): { param_0: string } {
return {param_0: this.translateUtil.translate(referrer) };
}
public getUri(): string {
return referrer_uri;
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Icon} from "./icon";
export type Active = "active" | "";
export class SideNavItem {
constructor(public displayName: string,
public link: string,
public tooltip?: string,
public icon?: Icon,
public active?: Active) {
}
setActive(active: Active) {
this.active = active;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import { Pipe, PipeTransform } from '@angular/core';
/**
* Case insensitive filtering.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Pipe({name: 'filterby'})
export class FilterbyPipe implements PipeTransform {
transform(objects: any[], property: string, text: string) {
if (!property) return objects;
if (!text) return objects;
const transformed: any[] = [];
for (let obj of objects) {
let propVal:any = obj[property];
if (!this.isString(propVal)) {
console.error("Can't filter property " + property + ". Its value is not a string.");
break;
}
let strPropVal:string = propVal as string;
if (strPropVal.toLowerCase().indexOf(text.toLowerCase()) != -1) {
transformed.push(obj);
}
}
return transformed;
}
private isString(value: any): boolean {
return (typeof value == 'string') || (value instanceof String);
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import { Pipe, PipeTransform } from '@angular/core';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Pipe({name: 'orderby'})
export class OrderbyPipe implements PipeTransform {
transform(objects: any[], property: string, descending: boolean = true) {
if (!property) return objects;
let sorted: any[] = objects.sort((obj1, obj2) => {
if (obj1[property] > obj2[property]) return 1;
if (obj1[property] < obj2[property]) return -1;
return 0;
})
if (descending) {
return sorted;
} else {
return sorted.reverse();
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
* For this application, we are responsive to three sizes: large, medium, and
* small. Note that these do not perfectly correspond to bootstrap device
* sizes but are more in line with patternfly.
*
* When making decisions based on screen size, you should create a single
* instance of this class and then test for each size. Do not use several
* instances because the screen size could change at any time and you may
* get inconsistent results.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
export class Media {
private screenWidth: number = window.innerWidth;
public isLarge(): boolean {
return this.screenWidth > 1023;
}
public isMedium(): boolean {
return (this.screenWidth < 1023) && (this.screenWidth > 768);
}
public isSmall(): boolean {
return this.screenWidth < 769;
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import { Injectable } from "@angular/core";
import { Media } from "./media";
export type SideNavClasses = "" | "collapsed" | "hidden" | "hidden show-mobile-nav";
export type ContentWidthClass = "" | "collapsed-nav" | "hidden-nav";
export interface MenuClickListener {
menuClicked(): void;
}
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Injectable()
export class ResponsivenessService {
private menuOn: boolean = false;
private menuListeners: MenuClickListener[] = Array<MenuClickListener>();
public addMenuClickListener(listener: MenuClickListener) {
this.menuListeners.push(listener);
}
public menuClicked() : void {
this.menuOn = !this.menuOn;
for (let listener of this.menuListeners) {
listener.menuClicked();
}
}
public calcSideNavWidthClasses() : SideNavClasses {
const media: Media = new Media();
if (media.isLarge() && !this.menuOn) {
return "";
}
if (media.isLarge() && this.menuOn) {
return "collapsed";
}
if (media.isMedium() && !this.menuOn) {
return "collapsed";
}
if (media.isMedium() && this.menuOn) {
return "";
}
// media must be small
if (!this.menuOn) {
return "hidden"
}
return "hidden show-mobile-nav";
}
public calcSideContentWidthClass() : ContentWidthClass {
const media: Media = new Media();
if (media.isLarge() && !this.menuOn) {
return "";
}
if (media.isLarge() && this.menuOn) {
return "collapsed-nav";
}
if (media.isMedium() && !this.menuOn) {
return "collapsed-nav";
}
if (media.isMedium() && this.menuOn) {
return "";
}
// media must be small
if (!this.menuOn) {
return "hidden-nav"
}
return "hidden-nav";
}
}

View file

@ -0,0 +1,71 @@
<nav class="nav-pf-vertical {{this.sideNavClasses}}"> <!-- {{this.sideNavClasses}} collapsed hidden show-mobile-nav -->
<ul class="list-group">
<li *ngFor="let item of navItems" class="list-group-item {{item.active}}">
<a [routerLink]="[item.link]">
<span class="{{item.icon.getClasses()}}" title="{{item.tooltip}}" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">{{item.displayName}}</span>
</a>
</li>
<li *ngIf="referrer.exists()" class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="{{referrer.getUri()}}">
<span class="pficon-arrow" title="{{'backTo' | translate:referrer.getName()}}"></span>
<span class="list-group-item-value">{{'backTo' | translate:referrer.getName()}}</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#" (click)="logout()">
<span class="fa fa-sign-out" title="{{'doSignOut' | translate}}"></span>
<span class="list-group-item-value">{{'doSignOut' | translate}}</span>
</a>
</li>
</ul>
</nav>
<!-- <li class="list-group-item active">
<a href="#">
<span class="pficon pficon-user" title="Dashboard" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Personal information</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="fa fa-link" title="My Services" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Connected Devices</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="fa fa-cubes" title="My Requests" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">Applications</span>
</a>
</li>
<li class="list-group-item">
<a href="#">
<span class="pficon pficon-history" title="My Items" data-toggle="tooltip" data-placement="right"></span>
<span class="list-group-item-value">History</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden">
<a href="#">
<span class="pficon pficon-help" title="Help"></span>
<span class="list-group-item-value">Help</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="fa fa-info-circle" title="About"></span>
<span class="list-group-item-value">About</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="pficon pficon-user" title="Preferences"></span>
<span class="list-group-item-value">Preferences</span>
</a>
</li>
<li class="list-group-item hidden-sm hidden-md hidden-lg">
<a href="#">
<span class="fa fa-sign-out" title="Log Out"></span>
<span class="list-group-item-value">Log Out</span>
</a>
</li>-->

View file

@ -0,0 +1,109 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit, HostListener} from '@angular/core';
import {Router, NavigationEnd} from '@angular/router';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import {TranslateUtil} from '../ngx-translate/translate.util';
import {SideNavItem, Active} from '../page/side-nav-item';
import {Icon} from '../page/icon';
import {ResponsivenessService, SideNavClasses, MenuClickListener} from "../responsiveness-service/responsiveness.service";
import {Media} from "../responsiveness-service/media";
import {Referrer} from "../page/referrer";
@Component({
selector: 'app-side-nav',
templateUrl: './side-nav.component.html',
styleUrls: ['./side-nav.component.css']
})
export class SideNavComponent implements OnInit, MenuClickListener {
private referrer: Referrer;
private sideNavClasses: SideNavClasses = this.respSvc.calcSideNavWidthClasses();
private isFirstRouterEvent: boolean = true;
public navItems: SideNavItem[];
constructor(private router: Router,
private translateUtil: TranslateUtil,
private respSvc: ResponsivenessService,
private keycloakService: KeycloakService) {
this.referrer = new Referrer(translateUtil);
this.navItems = [
this.makeSideNavItem("account", new Icon("pficon", "user"), "active"),
this.makeSideNavItem("password", new Icon("pficon", "key")),
this.makeSideNavItem("authenticator", new Icon("pficon", "cloud-security")),
this.makeSideNavItem("sessions", new Icon("fa", "clock-o")),
this.makeSideNavItem("applications", new Icon("fa", "th"))
];
this.router.events.subscribe(value => {
if (value instanceof NavigationEnd) {
const navEnd = value as NavigationEnd;
this.setActive(navEnd.url);
const media: Media = new Media();
if (media.isSmall() && !this.isFirstRouterEvent) {
this.respSvc.menuClicked();
}
this.isFirstRouterEvent = false;
}
});
this.respSvc.addMenuClickListener(this);
}
// use itemName for translate key, link, and tooltip
private makeSideNavItem(itemName: string, icon: Icon, active?: Active): SideNavItem {
const localizedName: string = this.translateUtil.translate(itemName);
return new SideNavItem(localizedName, itemName, localizedName, icon, active);
}
private logout() {
this.keycloakService.logout();
}
public menuClicked(): void {
this.sideNavClasses = this.respSvc.calcSideNavWidthClasses();
}
@HostListener('window:resize', ['$event'])
private onResize(event: any) {
this.sideNavClasses = this.respSvc.calcSideNavWidthClasses();
}
setActive(url: string) {
for (let navItem of this.navItems) {
if (("/" + navItem.link) === url) {
navItem.setActive("active");
} else {
navItem.setActive("");
}
}
if ("/" === url) {
this.navItems[0].setActive("active");
}
}
ngOnInit() {
}
}

View file

@ -0,0 +1,12 @@
<div *ngIf="isVisible" class="toast-pf
toast-pf-max-width
toast-pf-top-right
alert
{{notification.alertType}}
alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<span class="pficon {{notification.icon}}"></span>
{{notification.message}}
</div>

View file

@ -0,0 +1,43 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {ToastNotifier, ToastNotification} from './toast.notifier';
@Component({
selector: 'notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.css']
})
export class NotificationComponent {
private isVisible: boolean = false;
private notification: ToastNotification = new ToastNotification("");
constructor(toastNotifier: ToastNotifier) {
toastNotifier.subscribe((notification: ToastNotification) => {
this.notification = notification;
this.isVisible = true;
setTimeout(() => {
this.isVisible = false;
}, 8000);
})
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
import {Injectable, EventEmitter} from '@angular/core';
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2017 Red Hat Inc.
*/
@Injectable()
export class ToastNotifier extends EventEmitter<ToastNotification> {
constructor() {
super();
}
}
type ToastIcon = "pficon-ok" |
"pficon-info" |
"pficon-warning-triangle-o" |
"pficon-error-circle-o";
type ToastAlertType = "alert-success" |
"alert-info" |
"alert-warning" |
"alert-danger";
export type MessageType = "success" |
"info" |
"warning" |
"error";
export class ToastNotification {
public alertType: ToastAlertType = "alert-success";
public icon: ToastIcon = "pficon-ok";
constructor(public message: string, messageType?: MessageType) {
switch (messageType) {
case "info": {
this.alertType = "alert-info";
this.icon = "pficon-info";
break;
}
case "warning": {
this.alertType = "alert-warning";
this.icon = "pficon-warning-triangle-o";
break;
}
case "error": {
this.alertType = "alert-danger";
this.icon = "pficon-error-circle-o";
break;
}
default: {
this.alertType = "alert-success";
this.icon = "pficon-ok";
}
}
}
}

View file

@ -0,0 +1,47 @@
<nav class="navbar navbar-pf-alt">
<notification></notification>
<div class="navbar-header">
<button (click)="menuClicked()" type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="http://www.keycloak.org" class="navbar-brand">
<img class="navbar-brand-icon" type="image/svg+xml" src="{{resourceUrl}}/app/assets/img/keycloak-logo.png" alt="" width="auto" height="30px"/>
</a>
</div>
<nav class="collapse navbar-collapse">
<ul class="nav navbar-nav">
</ul>
<ul class="nav navbar-nav navbar-right navbar-iconic">
<li *ngIf="referrer.exists()">
<a class="nav-item-iconic" href="{{referrer.getUri()}}"><span class="pficon-arrow"></span> {{'backTo' | translate:referrer.getName()}}</a>
</li>
<li class="dropdown">
<a class="dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span title="Help" class="fa pficon-help"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li><a href="#">Help</a></li>
<li><a href="#">About</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li><a href="#">Preferences</a></li>
<li><a href="#" (click)="logout()">{{'doSignOut' | translate}}</a></li>
</ul>
</li>
</ul>
</nav>
</nav> <!--/.navbar-->

View file

@ -0,0 +1,53 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnInit} from '@angular/core';
import {TranslateUtil} from '../ngx-translate/translate.util';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import {ResponsivenessService} from "../responsiveness-service/responsiveness.service";
import {Referrer} from "../page/referrer";
declare const resourceUrl: string;
declare const referrer: string;
declare const referrer_uri: string;
@Component({
selector: 'app-top-nav',
templateUrl: './top-nav.component.html',
styleUrls: ['./top-nav.component.css']
})
export class TopNavComponent implements OnInit {
public resourceUrl: string = resourceUrl;
private referrer: Referrer;
constructor(private keycloakService: KeycloakService, translateUtil: TranslateUtil, private respSvc: ResponsivenessService) {
this.referrer = new Referrer(translateUtil);
}
private menuClicked(): void {
this.respSvc.menuClicked();
}
ngOnInit() {
}
private logout() {
this.keycloakService.logout();
}
}

View file

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View file

@ -0,0 +1,8 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
};

View file

@ -0,0 +1,46 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
//import { environment } from './environments/environment';
import { KeycloakService } from './app/keycloak-service/keycloak.service';
//if (environment.production) {
// enableProdMode();
//}
declare const authUrl: string;
declare const resourceUrl: string;
declare const realm: string;
const noLogin: boolean = false; // convenient for development
if (noLogin) {
platformBrowserDynamic().bootstrapModule(AppModule);
} else {
KeycloakService.init(authUrl + '/realms/' + realm + '/account/keycloak.json',
{onLoad: 'login-required'})
.then(() => {
platformBrowserDynamic().bootstrapModule(AppModule);
})
.catch((e: any) => {
console.log('Error in bootstrap: ' + JSON.stringify(e));
});
}

View file

@ -0,0 +1,61 @@
{
"name": "keycloak-preview",
"version": "1.0.0",
"description": "keycloak-preview account management written in Angular 2",
"scripts": {
"build": "tsc -p ./",
"build:watch": "tsc -p ./ -w",
"build:e2e": "tsc -p e2e/",
"serve": "lite-server -c=bs-config.json",
"serve:e2e": "lite-server -c=bs-config.e2e.json",
"prestart": "npm run build",
"start": "concurrently \"npm run build:watch\" \"npm run serve\"",
"pree2e": "npm run build:e2e",
"e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
"preprotractor": "webdriver-manager update",
"protractor": "protractor protractor.config.js",
"pretest": "npm run build",
"test": "concurrently \"npm run build:watch\" \"karma start karma.conf.js\"",
"pretest:once": "npm run build",
"test:once": "karma start karma.conf.js --single-run",
"lint": "tslint ./**/*.ts --exclude ./node_modules/** -t verbose"
},
"keywords": [],
"author": "Stan Silvert",
"license": "Apache 2.0",
"dependencies": {
"@angular/common": "~4.0.0",
"@angular/compiler": "~4.0.0",
"@angular/core": "~4.0.0",
"@angular/forms": "~4.0.0",
"@angular/http": "~4.0.0",
"@angular/platform-browser": "~4.0.0",
"@angular/platform-browser-dynamic": "~4.0.0",
"@angular/router": "~4.0.0",
"@ngx-translate/core": "^7.1.0",
"core-js": "^2.4.1",
"patternfly": "^3.23.2",
"rxjs": "^5.4.2",
"systemjs": "^0.20.17",
"zone.js": "^0.8.4"
},
"devDependencies": {
"@types/jasmine": "2.5.36",
"@types/node": "^6.0.46",
"canonical-path": "0.0.2",
"concurrently": "^3.2.0",
"jasmine-core": "~2.4.1",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.0.2",
"karma-jasmine-html-reporter": "^0.2.2",
"lite-server": "^2.2.2",
"lodash": "^4.16.4",
"protractor": "~4.0.14",
"rimraf": "^2.5.4",
"tslint": "^3.15.1",
"typescript": "^2.4.2"
},
"repository": {}
}

View file

@ -0,0 +1,5 @@
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}

View file

@ -0,0 +1,45 @@
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
module.exports.translate = function(load){
var url = document.createElement('a');
url.href = load.address;
var basePathParts = url.pathname.split('/');
basePathParts.pop();
var basePath = basePathParts.join('/');
var baseHref = document.createElement('a');
baseHref.href = this.baseURL;
baseHref = baseHref.pathname;
basePath = basePath.replace(baseHref, '');
load.source = load.source
.replace(templateUrlRegex, function(match, quote, url){
var resolvedUrl = url;
if (url.startsWith('.')) {
resolvedUrl = basePath + url.substr(1);
}
return 'templateUrl: "' + resolvedUrl + '"';
})
.replace(stylesRegex, function(match, relativeUrls) {
var urls = [];
while ((match = stringRegex.exec(relativeUrls)) !== null) {
if (match[2].startsWith('.')) {
urls.push('"' + basePath + match[2].substr(1) + '"');
} else {
urls.push('"' + match[2] + '"');
}
}
return "styleUrls: [" + urls.join(', ') + "]";
});
return load;
};

View file

@ -0,0 +1,11 @@
/**
* Add barrels and stuff
* Adjust as necessary for your application needs.
*/
// (function (global) {
// System.config({
// packages: {
// // add packages here
// }
// });
// })(this);

View file

@ -0,0 +1,49 @@
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': resourceUrl + '/node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
'app': resourceUrl + '/app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'@ngx-translate/core': 'npm:@ngx-translate/core/bundles/core.umd.js',
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
defaultExtension: 'js',
meta: {
'./*.js': {
loader: resourceUrl + '/systemjs-angular-loader.js'
}
}
},
'@ngx-translate/core': {
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}

View file

@ -0,0 +1,93 @@
{
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"indent": [
true,
"spaces"
],
"label-position": true,
"label-undefined": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
"static-before-instance",
"variables-before-functions"
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
parent=base
import=login/keycloak
deprecatedMode=false