KEYCLOAK-6508: Hide builtin pages

This commit is contained in:
Stan Silvert 2019-05-02 16:10:48 -04:00 committed by Bruno Oliveira da Silva
parent 4e09794e80
commit 68d7abac3a
14 changed files with 321 additions and 133 deletions

View file

@ -251,10 +251,10 @@
<h6>${msg("accountSecurityIntroMessage")}</h6> <h6>${msg("accountSecurityIntroMessage")}</h6>
</div> </div>
<div class="pf-c-card__body pf-c-content"> <div class="pf-c-card__body pf-c-content">
<h5 id="changePasswordLink"><a href="#/app/password">${msg("changePasswordHtmlTitle")}</a></h5> <h5 id="changePasswordLink"><a href="#/app/security/password">${msg("changePasswordHtmlTitle")}</a></h5>
<h5 id="authenticatorLink"><a href="#/app/authenticator">${msg("authenticatorTitle")}</a></h5> <h5 id="authenticatorLink"><a href="#/app/security/authenticator">${msg("authenticatorTitle")}</a></h5>
<h5 id="deviceActivityLink"><a href="#/app/device-activity">${msg("deviceActivityHtmlTitle")}</a></h5> <h5 id="deviceActivityLink"><a href="#/app/security/device-activity">${msg("deviceActivityHtmlTitle")}</a></h5>
<h5 id="linkedAccountsLink" style="display:none"><a href="#/app/linked-accounts">${msg("linkedAccountsHtmlTitle")}</a></h5> <h5 id="linkedAccountsLink" style="display:none"><a href="#/app/security/linked-accounts">${msg("linkedAccountsHtmlTitle")}</a></h5>
</div> </div>
</div> </div>
</div> </div>

View file

@ -23,6 +23,7 @@ module.exports = {
rules: { rules: {
"no-useless-constructor": "off", "no-useless-constructor": "off",
"@typescript-eslint/indent": "off", "@typescript-eslint/indent": "off",
"@typescript-eslint/no-empty-interface" : "off" "@typescript-eslint/no-empty-interface": "off",
"no-restricted-properties": "off"
}, },
}; };

View file

@ -18,3 +18,4 @@ node
!systemjs.config.js !systemjs.config.js
!.eslintrc.js !.eslintrc.js
!WelcomePageScripts.js !WelcomePageScripts.js
!content.js

View file

@ -16,12 +16,12 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import {System} from 'systemjs';
import {HashRouter} from 'react-router-dom'; import {HashRouter} from 'react-router-dom';
import {App} from './app/App'; import {App} from './app/App';
import {ContentItem, ModulePageDef, flattenContent, initGroupAndItemIds, isExpansion, isModulePageDef} from './app/ContentPages';
const e = React.createElement;
export interface MainProps {} export interface MainProps {}
export class Main extends React.Component<MainProps> { export class Main extends React.Component<MainProps> {
@ -39,5 +39,54 @@ export class Main extends React.Component<MainProps> {
} }
}; };
declare const resourceUrl: string;
declare let content: ContentItem[];
const e = React.createElement;
function removeHidden(items: ContentItem[]): ContentItem[] {
const visible: ContentItem[] = [];
for (let item of items) {
if (item.hidden) continue;
if (isExpansion(item)) {
visible.push(item);
item.content = removeHidden(item.content);
if (item.content.length === 0) {
visible.pop(); // remove empty expansion
}
} else {
visible.push(item);
}
}
return visible;
}
content = removeHidden(content);
initGroupAndItemIds();
function loadModule(modulePage: ModulePageDef): Promise<ModulePageDef> {
return new Promise ((resolve, reject) => {
System.import(resourceUrl + modulePage.modulePath).then( (module: React.Component) => {
modulePage.module = module;
resolve(modulePage);
}).catch((error: Error) => {
console.warn('Unable to load ' + modulePage.label + ' because ' + error.message);
reject(modulePage);
});
});
};
const moduleLoaders: Promise<ModulePageDef>[] = [];
flattenContent(content).forEach((item: ContentItem) => {
if (isModulePageDef(item)) {
moduleLoaders.push(loadModule(item));
}
});
// load content modules and start
Promise.all(moduleLoaders).then(() => {
const domContainer = document.querySelector('#main_react_container'); const domContainer = document.querySelector('#main_react_container');
ReactDOM.render(e(Main), domContainer); ReactDOM.render(e(Main), domContainer);
});

View file

@ -15,7 +15,6 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {Route} from 'react-router-dom';
import * as moment from 'moment'; import * as moment from 'moment';
@ -24,15 +23,7 @@ import {KeycloakService} from './keycloak-service/keycloak.service';
import {PageNav} from './PageNav'; import {PageNav} from './PageNav';
import {PageToolbar} from './PageToolbar'; import {PageToolbar} from './PageToolbar';
import {Background} from './Background'; import {Background} from './Background';
import {makeRoutes} from './ContentPages';
import {AccountPage} from './content/account-page/AccountPage';
import {PasswordPage} from './content/password-page/PasswordPage';
import {AuthenticatorPage} from './content/authenticator-page/AuthenticatorPage';
import {DeviceActivityPage} from './content/device-activity-page/DeviceActivityPage';
import {LinkedAccountsPage} from './content/linked-accounts-page/LinkedAccountsPage';
import {ApplicationsPage} from './content/applications-page/ApplicationsPage';
import {MyResourcesPage} from './content/my-resources-page/MyResourcesPage';
import {ExtensionPages} from './content/extensions/ExtensionPages';
import { import {
Avatar, Avatar,
@ -89,14 +80,7 @@ export class App extends React.Component<AppProps> {
<Background/> <Background/>
<Page header={Header} sidebar={Sidebar} isManagedSidebar> <Page header={Header} sidebar={Sidebar} isManagedSidebar>
<PageSection> <PageSection>
<Route path='/app/account' component={AccountPage} /> {makeRoutes()}
<Route path='/app/password' component={PasswordPage} />
<Route path='/app/authenticator' component={AuthenticatorPage} />
<Route path='/app/device-activity' component={DeviceActivityPage} />
<Route path='/app/linked-accounts' component={LinkedAccountsPage} />
<Route path='/app/applications' component={ApplicationsPage} />
<Route path='/app/my-resources' component={MyResourcesPage} />
{ExtensionPages.Routes}
</PageSection> </PageSection>
</Page> </Page>
</React.Fragment> </React.Fragment>

View file

@ -0,0 +1,163 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates.
*
* 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 * as React from 'react';
import {Route} from 'react-router-dom';
import {NavItem, NavExpandable} from '@patternfly/react-core';
import {Msg} from './widgets/Msg';
export interface ContentItem {
label: string;
labelParams?: string[];
hidden?: boolean;
groupId: string; // computed value
itemId: string; // computed value
};
export interface Expansion extends ContentItem {
content: ContentItem[];
}
export interface PageDef extends ContentItem {
path: string;
}
export interface ComponentPageDef extends PageDef {
component: React.ComponentType;
}
export interface ModulePageDef extends PageDef {
modulePath: string;
componentName: string;
module: React.Component; // computed value
}
export function isModulePageDef(item: ContentItem): item is ModulePageDef {
return (item as ModulePageDef).modulePath !== undefined;
}
export function isExpansion(contentItem: ContentItem): contentItem is Expansion {
return (contentItem as Expansion).content !== undefined;
}
declare const content: ContentItem[];
function groupId(group: number): string {
return 'grp-' + group;
}
function itemId(group: number, item: number): string {
return 'grp-' + group + '_itm-' + item;
}
function isChildOf(parent: Expansion, child: PageDef): boolean {
for (var item of parent.content) {
if (isExpansion(item) && isChildOf(item, child)) return true;
if (parent.groupId === child.groupId) return true;
}
return false;
}
function createNavItems(activePage: PageDef, contentParam: ContentItem[], groupNum: number): React.ReactNode {
if (typeof content === 'undefined') return (<React.Fragment/>);
const links: React.ReactElement[] = contentParam.map((item: ContentItem) => {
if (isExpansion(item)) {
return <NavExpandable groupId={item.groupId}
key={item.groupId}
title={Msg.localize(item.label, item.labelParams)}
isExpanded={isChildOf(item, activePage)}>
{createNavItems(activePage, item.content, groupNum + 1)}
</NavExpandable>
} else {
const page: PageDef = item as PageDef;
return <NavItem groupId={item.groupId}
itemId={item.itemId}
key={item.itemId}
to={'#/app/' + page.path}
isActive={activePage.itemId === item.itemId}
type="button">
{Msg.localize(page.label, page.labelParams)}
</NavItem>
}
});
return (<React.Fragment>{links}</React.Fragment>);
}
export function makeNavItems(activePage: PageDef): React.ReactNode {
console.log({activePage});
return createNavItems(activePage, content, 0);
}
function setIds(contentParam: ContentItem[], groupNum: number): number {
if (typeof contentParam === 'undefined') return groupNum;
let expansionGroupNum = groupNum;
for (let i = 0; i < contentParam.length; i++) {
const item: ContentItem = contentParam[i];
if (isExpansion(item)) {
item.itemId = itemId(groupNum, i);
expansionGroupNum = expansionGroupNum + 1;
item.groupId = groupId(expansionGroupNum);
expansionGroupNum = setIds(item.content, expansionGroupNum);
console.log('currentGroup=' + (expansionGroupNum));
} else {
item.groupId = groupId(groupNum);
item.itemId = itemId(groupNum, i);
}
};
return expansionGroupNum;
}
export function initGroupAndItemIds(): void {
setIds(content, 0);
console.log({content});
}
// get rid of Expansions and put all PageDef items into a single array
export function flattenContent(pageDefs: ContentItem[]): PageDef[] {
const flat: PageDef[] = [];
for (let item of pageDefs) {
if (isExpansion(item)) {
flat.push(...flattenContent(item.content));
} else {
flat.push(item as PageDef);
}
}
return flat;
}
export function makeRoutes(): React.ReactNode {
if (typeof content === 'undefined') return (<span/>);
const pageDefs: PageDef[] = flattenContent(content);
const routes: React.ReactElement<Route>[] = pageDefs.map((page: PageDef) => {
if (isModulePageDef(page)) {
return <Route key={page.itemId} path={'/app/' + page.path} component={page.module[page.componentName]}/>;
} else {
const pageDef: ComponentPageDef = page as ComponentPageDef;
return <Route key={page.itemId} path={'/app/' + page.path} component={pageDef.component}/>;
}
});
return (<React.Fragment>{routes}</React.Fragment>);
}

View file

@ -15,67 +15,47 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {Nav, NavList} from '@patternfly/react-core';
import {Nav, NavExpandable, NavList, NavItem} from '@patternfly/react-core'; import {makeNavItems, flattenContent, ContentItem, PageDef} from './ContentPages';
import {Msg} from './widgets/Msg'; declare const content: ContentItem[];
import {ExtensionPages} from './content/extensions/ExtensionPages';
export interface PageNavProps { export interface PageNavProps extends RouteComponentProps {}
}
interface PageNavState { export interface PageNavState {}
activeGroup: string | number;
activeItem: string | number;
}
export class PageNav extends React.Component<PageNavProps, PageNavState> { class PageNavigation extends React.Component<PageNavProps, PageNavState> {
public constructor(props: PageNavProps) { public constructor(props: PageNavProps) {
super(props); super(props);
this.state = {
activeGroup: '',
activeItem: 'grp-0_itm-0'
};
} }
private onNavSelect = (groupId: number, itemId: number): void => { private findActiveItem(): PageDef {
this.setState({ const currentPath: string = this.props.location.pathname;
activeItem: itemId, const items: PageDef[] = flattenContent(content);
activeGroup: groupId const firstItem = items[0];
}); for (let item of items) {
const itemPath: string = '/app/' + item.path;
if (itemPath === currentPath) {
return item;
}
}; };
return firstItem;
}
public render(): React.ReactNode { public render(): React.ReactNode {
const activeItem: PageDef = this.findActiveItem();
return ( return (
<Nav onSelect={this.onNavSelect} aria-label="Nav"> <Nav aria-label="Nav">
<NavList> <NavList>
<NavItem to="#/app/account" itemId="grp-0_itm-0" isActive={this.state.activeItem === 'grp-0_itm-0'}> {makeNavItems(activeItem)}
{Msg.localize("account")}
</NavItem>
<NavExpandable title="Account Security" groupId="grp-1" isActive={this.state.activeGroup === 'grp-1'}>
<NavItem to="#/app/password" groupId="grp-1" itemId="grp-1_itm-1" isActive={this.state.activeItem === 'grp-1_itm-1'}>
{Msg.localize("password")}
</NavItem>
<NavItem to="#/app/authenticator" groupId="grp-1" itemId="grp-1_itm-2" isActive={this.state.activeItem === 'grp-1_itm-2'}>
{Msg.localize("authenticator")}
</NavItem>
<NavItem to="#/app/device-activity" groupId="grp-1" itemId="grp-1_itm-3" isActive={this.state.activeItem === 'grp-1_itm-3'}>
{Msg.localize("device-activity")}
</NavItem>
<NavItem to="#/app/linked-accounts" groupId="grp-1" itemId="grp-1_itm-4" isActive={this.state.activeItem === 'grp-1_itm-4'}>
{Msg.localize("linkedAccountsHtmlTitle")}
</NavItem>
</NavExpandable>
<NavItem to="#/app/applications" itemId="grp-2_itm-0" isActive={this.state.activeItem === 'grp-2_itm-0'}>
{Msg.localize("applications")}
</NavItem>
<NavItem to="#/app/my-resources" itemId="grp-3_itm-0" isActive={this.state.activeItem === 'grp-3_itm-0'}>
{Msg.localize("myResources")}
</NavItem>
{ExtensionPages.Links}
</NavList> </NavList>
</Nav> </Nav>
); );
} }
} }
export const PageNav = withRouter(PageNavigation);

View file

@ -1,49 +0,0 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates.
*
* 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 * as React from 'react';
import {Route} from 'react-router-dom';
import {NavItem} from '@patternfly/react-core';
export interface PageDef {
path: string;
label: string;
component: React.ComponentType;
}
declare const extensionPages: PageDef[];
export class ExtensionPages { // extends React.Component<ExtensionPagesProps> {
public static get Links(): React.ReactNode {
if (typeof extensionPages === 'undefined') return (<span/>);
const links: React.ReactElement[] = extensionPages.map((page: PageDef, index: number) =>
<NavItem key={page.path} to={'#/app/' + page.path} itemId={'ext-' + index} type="button">{page.label}</NavItem>
);
return (<React.Fragment>{links}</React.Fragment>);
}
public static get Routes(): React.ReactNode {
if (typeof extensionPages === 'undefined') return (<span/>);
const routes: React.ReactElement<Route>[] = extensionPages.map((page) =>
<Route key={page.path} path={'/app/' + page.path} component={page.component}/>
);
return (<React.Fragment>{routes}</React.Fragment>);
}
};

View file

@ -39,7 +39,7 @@ export class Msg extends React.Component<MsgProps> {
let message: string = l18nMsg[msgKey]; let message: string = l18nMsg[msgKey];
if (message === undefined) message = msgKey; if (message === undefined) message = msgKey;
if (params !== undefined) { if ((params !== undefined) && (params.length > 0)) {
params.forEach((value: string, index: number) => { params.forEach((value: string, index: number) => {
value = this.processParam(value); value = this.processParam(value);
message = message.replace('{{param_'+ index + '}}', value); message = message.replace('{{param_'+ index + '}}', value);

View file

@ -0,0 +1,52 @@
var content = [
{
path: 'account',
label: 'account',
modulePath: '/app/content/account-page/AccountPage',
componentName: 'AccountPage'
},
{
label: 'Account Security',
content: [
{
path: 'security/password',
label: 'password',
modulePath: '/app/content/password-page/PasswordPage',
componentName: 'PasswordPage'
},
{
path: 'security/authenticator',
label: 'authenticator',
modulePath: '/app/content/authenticator-page/AuthenticatorPage',
componentName: 'AuthenticatorPage'
},
{
path: 'security/device-activity',
label: 'device-activity',
modulePath: '/app/content/device-activity-page/DeviceActivityPage',
componentName: 'DeviceActivityPage'
},
{
path: 'security/linked-accounts',
label: 'linkedAccountsHtmlTitle',
modulePath: '/app/content/linked-accounts-page/LinkedAccountsPage',
componentName: 'LinkedAccountsPage',
hidden: !features.isLinkedAccountsEnabled
}
]
},
{
path: 'applications',
label: 'applications',
modulePath: '/app/content/applications-page/ApplicationsPage',
componentName: 'ApplicationsPage'
},
{
path: 'my-resources',
label: 'myResources',
modulePath: '/app/content/my-resources-page/MyResourcesPage',
componentName: 'MyResourcesPage',
hidden: !features.isMyResourcesEnabled
}
];

View file

@ -305,6 +305,12 @@
"@types/react-router": "*" "@types/react-router": "*"
} }
}, },
"@types/systemjs": {
"version": "0.20.6",
"resolved": "https://registry.npmjs.org/@types/systemjs/-/systemjs-0.20.6.tgz",
"integrity": "sha512-p3yv9sBBJXi3noUG216BpUI7VtVBUAvBIfZNTiDROUY31YBfsFHM4DreS7XMekN8IjtX0ysvCnm6r3WnirnNeA==",
"dev": true
},
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.4.2.tgz",
@ -551,7 +557,7 @@
}, },
"axios": { "axios": {
"version": "0.18.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": { "requires": {
"follow-redirects": "^1.3.0", "follow-redirects": "^1.3.0",
@ -2602,9 +2608,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
}, },
"js-yaml": { "js-yaml": {
"version": "3.13.0", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"

View file

@ -25,6 +25,7 @@
"@types/react": "^16.8.8", "@types/react": "^16.8.8",
"@types/react-dom": "^16.8.3", "@types/react-dom": "^16.8.3",
"@types/react-router-dom": "^4.3.1", "@types/react-router-dom": "^4.3.1",
"@types/systemjs": "^0.20.6",
"@typescript-eslint/eslint-plugin": "^1.4.2", "@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2", "@typescript-eslint/parser": "^1.4.2",
"babel-eslint": "^9.0.0", "babel-eslint": "^9.0.0",

View file

@ -1,4 +1,4 @@
parent=base parent=base
deprecatedMode=false deprecatedMode=false
scripts=WelcomePageScripts.js scripts=WelcomePageScripts.js content.js
#developmentMode=true #developmentMode=true