KEYCLOAK-9644: Implement Nav and Headers using PF4 React

This commit is contained in:
Stan Silvert 2019-04-12 12:52:30 -04:00 committed by Bruno Oliveira da Silva
parent 81a37d3496
commit 2736dd9d61
16 changed files with 2743 additions and 219 deletions

View file

@ -7,7 +7,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script> <script>
var authUrl = '${authUrl}'; var authUrl = '${authUrl}';
var baseUrl = '${baseUrl}'; var baseUrl = '${baseUrl}';
@ -68,13 +68,6 @@
<!-- TODO: We should save these css and js into variables and then load in <!-- TODO: We should save these css and js into variables and then load in
main.ts for better performance. These might be loaded twice. main.ts for better performance. These might be loaded twice.
--> -->
<#if properties.styles?has_content>
<#list properties.styles?split(' ') as style>
<link href="${resourceUrl}/${style}" rel="stylesheet"/>
</#list>
<a href="../../../../../../../../keycloak-quickstarts/app-profile-jee-html5/src/main/webapp/index.html"></a>
</#if>
<#if properties.scripts?has_content> <#if properties.scripts?has_content>
<#list properties.scripts?split(' ') as script> <#list properties.scripts?split(' ') as script>
<script type="text/javascript" src="${resourceUrl}/${script}"></script> <script type="text/javascript" src="${resourceUrl}/${script}"></script>
@ -113,22 +106,38 @@
<div id="main_react_container"></div> <div id="main_react_container"></div>
<div id="welcomeScreen" style="display:none"> <div id="welcomeScreen" style="display:none">
<div class="pf-c-background-image"> <#if properties.styles?has_content>
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> <#list properties.styles?split(' ') as style>
<filter id="image_overlay" width=""> <link href="${resourceUrl}/${style}" rel="stylesheet"/>
<feColorMatrix type="matrix" values="1 0 0 0 0 </#list>
1 0 0 0 0 </#if>
1 0 0 0 0 <style>
0 0 0 1 0" /> .pf-c-background-image {
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone"> --pf-c-background-image--BackgroundImage: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/pfbg_576.jpg');
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR> --pf-c-background-image--BackgroundImage-2x: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/pfbg_576@2x.jpg');
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG> --pf-c-background-image--BackgroundImage--sm: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/pfbg_768.jpg');
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB> --pf-c-background-image--BackgroundImage--sm-2x: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/pfbg_768@2x.jpg');
<feFuncA type="table" tableValues="0 1"></feFuncA> --pf-c-background-image--BackgroundImage--lg: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/pfbg_1200.jpg');
</feComponentTransfer> --pf-c-background-image--Filter: url('${resourceUrl}/node_modules/@patternfly/patternfly/assets/images/background-filter.svg#image_overlay');
</filter> }
</svg> </style>
</div>
<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay" width="">
<feColorMatrix type="matrix" values="1 0 0 0 0
1 0 0 0 0
1 0 0 0 0
0 0 0 1 0" />
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-page" id="page-layout-default-nav"> <div class="pf-c-page" id="page-layout-default-nav">
<header role="banner" class="pf-c-page__header"> <header role="banner" class="pf-c-page__header">
<div class="pf-c-page__header-brand"> <div class="pf-c-page__header-brand">

View file

@ -15,15 +15,15 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {Route, Link} from 'react-router-dom'; import {Route} from 'react-router-dom';
import * as moment from 'moment'; import * as moment from 'moment';
import {KeycloakService} from './keycloak-service/keycloak.service'; import {KeycloakService} from './keycloak-service/keycloak.service';
import {Logout} from './widgets/Logout'; import {PageNav} from './PageNav';
import {Msg} from './widgets/Msg'; import {PageToolbar} from './PageToolbar';
import {Referrer} from './page/Referrer'; import {Background} from './Background';
import {AccountPage} from './content/account-page/AccountPage'; import {AccountPage} from './content/account-page/AccountPage';
import {PasswordPage} from './content/password-page/PasswordPage'; import {PasswordPage} from './content/password-page/PasswordPage';
@ -34,57 +34,73 @@ import {ApplicationsPage} from './content/applications-page/ApplicationsPage';
import {MyResourcesPage} from './content/my-resources-page/MyResourcesPage'; import {MyResourcesPage} from './content/my-resources-page/MyResourcesPage';
import {ExtensionPages} from './content/extensions/ExtensionPages'; import {ExtensionPages} from './content/extensions/ExtensionPages';
import {
Avatar,
Brand,
Page,
PageHeader,
PageSection,
PageSidebar,
} from '@patternfly/react-core';
declare function toggleReact(): void; declare function toggleReact(): void;
declare function isWelcomePage(): boolean; declare function isWelcomePage(): boolean;
declare const locale: string; declare const locale: string;
declare const resourceUrl: string;
const pFlyImages = resourceUrl + '/node_modules/@patternfly/patternfly/assets/images/';
const brandImg = resourceUrl + '/app/assets/img/keycloak-logo-min.png';
const avatarImg = pFlyImages + 'img_avatar.svg';
export interface AppProps {}; export interface AppProps {};
export class App extends React.Component<AppProps> { export class App extends React.Component<AppProps> {
private kcSvc: KeycloakService = KeycloakService.Instance; private kcSvc: KeycloakService = KeycloakService.Instance;
public constructor(props: AppProps) { public constructor(props: AppProps) {
super(props); super(props);
console.log('Called into App constructor'); console.log('Called into App constructor');
toggleReact(); toggleReact();
} }
public render(): React.ReactNode { public render(): React.ReactNode {
toggleReact(); toggleReact();
// check login // check login
if (!this.kcSvc.authenticated() && !isWelcomePage()) { if (!this.kcSvc.authenticated() && !isWelcomePage()) {
this.kcSvc.login(); this.kcSvc.login();
} }
// globally set up locale for date formatting // globally set up locale for date formatting
moment.locale(locale); moment.locale(locale);
const Header = (
<PageHeader
logo={<Brand src={brandImg} alt="Patternfly Logo" />}
toolbar={<PageToolbar/>}
avatar={<Avatar src={avatarImg} alt="Avatar image" />}
showNavToggle
/>
);
const Sidebar = <PageSidebar nav={<PageNav/>} />;
return ( return (
<span> <React.Fragment>
<Referrer/> <Background/>
<nav> <Page header={Header} sidebar={Sidebar} isManagedSidebar>
<Link to="/app/account" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="account"/></Link> <PageSection>
<Link to="/app/password" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="password"/></Link> <Route path='/app/account' component={AccountPage} />
<Link to="/app/authenticator" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="authenticator"/></Link> <Route path='/app/password' component={PasswordPage} />
<Link to="/app/device-activity" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="device-activity"/></Link> <Route path='/app/authenticator' component={AuthenticatorPage} />
<Link to="/app/linked-accounts" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="linkedAccountsHtmlTitle"/></Link> <Route path='/app/device-activity' component={DeviceActivityPage} />
<Link to="/app/applications" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="applications"/></Link> <Route path='/app/linked-accounts' component={LinkedAccountsPage} />
<Link to="/app/my-resources" className="pf-c-button pf-m-primary" type="button"><Msg msgKey="myResources"/></Link> <Route path='/app/applications' component={ApplicationsPage} />
{ExtensionPages.Links} <Route path='/app/my-resources' component={MyResourcesPage} />
<Logout/> {ExtensionPages.Routes}
<Route path='/app/account' component={AccountPage}/> </PageSection>
<Route path='/app/password' component={PasswordPage}/> </Page>
<Route path='/app/authenticator' component={AuthenticatorPage}/> </React.Fragment>
<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}
</nav>
</span>
); );
} }
}; };

View file

@ -0,0 +1,46 @@
/*
* 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 {BackgroundImageSrc, BackgroundImage} from '@patternfly/react-core';
declare const resourceUrl: string;
const pFlyImages = resourceUrl + '/node_modules/@patternfly/patternfly/assets/images/';
const bgImages = {
[BackgroundImageSrc.xs]: pFlyImages + 'pfbg_576.jpg',
[BackgroundImageSrc.xs2x]: pFlyImages + 'pfbg_576@2x.jpg',
[BackgroundImageSrc.sm]: pFlyImages + 'pfbg_768.jpg',
[BackgroundImageSrc.sm2x]: pFlyImages + 'pfbg_768@2x.jpg',
[BackgroundImageSrc.lg]: pFlyImages + 'pfbg_1200.jpg',
[BackgroundImageSrc.filter]: pFlyImages + 'background-filter.svg#image_overlay'
};
interface BackgroundProps {}
export class Background extends React.Component<BackgroundProps> {
public constructor(props: BackgroundProps) {
super(props);
}
public render(): React.ReactNode {
return (
<BackgroundImage src={bgImages} />
);
}
};

View file

@ -0,0 +1,81 @@
/*
* 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 {Nav, NavExpandable, NavList, NavItem} from '@patternfly/react-core';
import {Msg} from './widgets/Msg';
import {ExtensionPages} from './content/extensions/ExtensionPages';
export interface PageNavProps {
}
interface PageNavState {
activeGroup: string | number;
activeItem: string | number;
}
export class PageNav extends React.Component<PageNavProps, PageNavState> {
public constructor(props: PageNavProps) {
super(props);
this.state = {
activeGroup: '',
activeItem: 'grp-0_itm-0'
};
}
private onNavSelect = (groupId: number, itemId: number): void => {
this.setState({
activeItem: itemId,
activeGroup: groupId
});
};
public render(): React.ReactNode {
return (
<Nav onSelect={this.onNavSelect} aria-label="Nav">
<NavList>
<NavItem to="#/app/account" itemId="grp-0_itm-0" isActive={this.state.activeItem === 'grp-0_itm-0'}>
{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>
</Nav>
);
}
}

View file

@ -0,0 +1,97 @@
/*
* 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 {Dropdown, KebabToggle, Toolbar, ToolbarGroup, ToolbarItem} from '@patternfly/react-core';
import {ReferrerDropdownItem} from './widgets/ReferrerDropdownItem';
import {ReferrerLink} from './widgets/ReferrerLink';
import {Features} from './widgets/features';
import {LocaleNav,LocaleDropdown} from './widgets/LocaleSelectors';
import {LogoutButton,LogoutDropdownItem} from './widgets/Logout';
declare const referrerName: string;
declare const features: Features;
interface PageToolbarProps {}
interface PageToolbarState {isKebabDropdownOpen: boolean}
export class PageToolbar extends React.Component<PageToolbarProps, PageToolbarState> {
private hasReferrer: boolean = typeof referrerName !== 'undefined';
public constructor(props: PageToolbarProps) {
super(props);
this.state = {
isKebabDropdownOpen: false,
};
}
private onKebabDropdownToggle = (isKebabDropdownOpen: boolean) => {
this.setState({
isKebabDropdownOpen
});
};
public render(): React.ReactNode {
const kebabDropdownItems = [];
if (this.hasReferrer) {
kebabDropdownItems.push(
<ReferrerDropdownItem key='referrerDropdownItem'/>
)
}
if (features.isInternationalizationEnabled) {
kebabDropdownItems.push(<LocaleNav key='kebabLocaleNav'/>);
}
kebabDropdownItems.push(<LogoutDropdownItem key='LogoutDropdownItem'/>);
return (
<Toolbar>
{this.hasReferrer &&
<ToolbarGroup key='referrerGroup'>
<ToolbarItem className="pf-m-icons pf-screen-reader" key='referrer'>
<ReferrerLink/>
</ToolbarItem>
</ToolbarGroup>
}
<ToolbarGroup key='secondGroup'>
{features.isInternationalizationEnabled &&
<ToolbarItem className="pf-m-icons pf-screen-reader" key='locale'>
<LocaleDropdown/>
</ToolbarItem>
}
<ToolbarItem className="pf-m-icons pf-screen-reader" key='logout'>
<LogoutButton/>
</ToolbarItem>
<ToolbarItem key='kebab' className="pf-m-mobile">
<Dropdown
isPlain
position="right"
toggle={<KebabToggle onToggle={this.onKebabDropdownToggle} />}
isOpen={this.state.isKebabDropdownOpen}
dropdownItems={kebabDropdownItems}
/>
</ToolbarItem>
</ToolbarGroup>
</Toolbar>
);
}
}

View file

@ -18,7 +18,7 @@ import * as React from 'react';
import {AxiosResponse} from 'axios'; import {AxiosResponse} from 'axios';
import {AccountServiceClient} from '../../account-service/account.service'; import {AccountServiceClient} from '../../account-service/account.service';
import {Features} from '../../page/features'; import {Features} from '../../widgets/features';
import {Msg} from '../../widgets/Msg'; import {Msg} from '../../widgets/Msg';
declare const features: Features; declare const features: Features;

View file

@ -15,7 +15,8 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {Route, Link} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {NavItem} from '@patternfly/react-core';
export interface PageDef { export interface PageDef {
path: string; path: string;
@ -30,10 +31,10 @@ export class ExtensionPages { // extends React.Component<ExtensionPagesProps> {
public static get Links(): React.ReactNode { public static get Links(): React.ReactNode {
if (typeof extensionPages === 'undefined') return (<span/>); if (typeof extensionPages === 'undefined') return (<span/>);
const links: React.ReactElement<Link>[] = extensionPages.map((page: PageDef) => const links: React.ReactElement[] = extensionPages.map((page: PageDef, index: number) =>
<Link key={page.path} to={'/app/' + page.path} className="btn btn-primary btn-lg btn-sign" type="button">{page.label}</Link> <NavItem key={page.path} to={'#/app/' + page.path} itemId={'ext-' + index} type="button">{page.label}</NavItem>
); );
return (<span>{links}</span>); return (<React.Fragment>{links}</React.Fragment>);
} }
public static get Routes(): React.ReactNode { public static get Routes(): React.ReactNode {
@ -42,7 +43,7 @@ export class ExtensionPages { // extends React.Component<ExtensionPagesProps> {
const routes: React.ReactElement<Route>[] = extensionPages.map((page) => const routes: React.ReactElement<Route>[] = extensionPages.map((page) =>
<Route key={page.path} path={'/app/' + page.path} component={page.component}/> <Route key={page.path} path={'/app/' + page.path} component={page.component}/>
); );
return (<span>{routes}</span>); return (<React.Fragment>{routes}</React.Fragment>);
} }
}; };

View file

@ -0,0 +1,126 @@
/*
* 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 {withRouter, RouteComponentProps} from 'react-router-dom';
import {Msg} from './Msg';
import {
Dropdown,
DropdownItem,
DropdownToggle,
Nav,
NavExpandable,
NavItem,
NavList
} from '@patternfly/react-core';
declare const locale: string;
declare const baseUrl: string;
declare const referrer: string;
declare const referrerUri: string
interface AvailableLocale {
locale: string;
label: string;
};
declare const availableLocales: [AvailableLocale];
// remove entry for current locale
const availLocales = availableLocales.filter((availableLocale: AvailableLocale) => availableLocale.locale !== locale);
let referrerFragment = '';
if ((typeof referrer !== 'undefined') &&
(typeof referrerUri !== 'undefined')) {
referrerFragment = '&referrer=' + referrer + '&referrer_uri=' + encodeURIComponent(referrerUri);
}
interface LocaleKebabItemProps extends RouteComponentProps {}
interface LocaleKebabItemState {activeGroup: string; activeItem: string}
class LocaleKebabItem extends React.Component<LocaleKebabItemProps, LocaleKebabItemState> {
public constructor(props: LocaleKebabItemProps) {
super(props);
this.state = {
activeGroup: 'locale-group',
activeItem: ''
};
}
public render(): React.ReactNode {
const appPath = this.props.location.pathname;
const localeNavItems = availLocales.map((availableLocale: AvailableLocale) => {
const url = baseUrl + '?kc_locale=' + availableLocale.locale + referrerFragment + '#' + appPath;
return (<NavItem key={availableLocale.locale} to={url}>
{availableLocale.label}
</NavItem> );
});
return (
<Nav>
<NavList>
<NavExpandable title={Msg.localize('locale_' + locale)} isActive={false} groupId="locale-group">
{localeNavItems}
</NavExpandable>
</NavList>
</Nav>
);
}
};
interface LocaleDropdownComponentProps extends RouteComponentProps {}
interface LocaleDropdownComponentState {isDropdownOpen: boolean}
class LocaleDropdownComponent extends React.Component<LocaleDropdownComponentProps, LocaleDropdownComponentState> {
public constructor(props: LocaleDropdownComponentProps) {
super(props);
this.state = {isDropdownOpen: false};
}
private onDropdownToggle = (isDropdownOpen: boolean) => {
this.setState({
isDropdownOpen
});
};
private onDropdownSelect = () => {
this.setState({
isDropdownOpen: !this.state.isDropdownOpen
});
};
public render(): React.ReactNode {
const appPath = this.props.location.pathname;
const localeDropdownItems = availLocales.map((availableLocale: AvailableLocale) => {
const url = baseUrl + '?kc_locale=' + availableLocale.locale + referrerFragment + '#' + appPath;
return (<DropdownItem key={availableLocale.locale} href={url}>
{availableLocale.label}
</DropdownItem> );
});
return (
<Dropdown
isPlain
position="right"
onSelect={this.onDropdownSelect}
isOpen={this.state.isDropdownOpen}
toggle={<DropdownToggle onToggle={this.onDropdownToggle}><Msg msgKey={'locale_' + locale}/></DropdownToggle>}
dropdownItems={localeDropdownItems}
/>
);
}
};
export const LocaleDropdown = withRouter(LocaleDropdownComponent);
export const LocaleNav = withRouter(LocaleKebabItem);

View file

@ -15,29 +15,34 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {Link} from 'react-router-dom';
import {Msg} from './Msg'; import {Msg} from './Msg';
import {KeycloakService} from '../keycloak-service/keycloak.service'; import {KeycloakService} from '../keycloak-service/keycloak.service';
import {Button, DropdownItem} from '@patternfly/react-core';
declare const baseUrl: string; declare const baseUrl: string;
export interface LogoutProps { function handleLogout(): void {
KeycloakService.Instance.logout(baseUrl);
} }
export class Logout extends React.Component<LogoutProps> {
public constructor(props: LogoutProps) {
super(props);
}
private handleLogout(): void {
KeycloakService.Instance.logout(baseUrl);
}
interface LogoutProps {}
export class LogoutButton extends React.Component<LogoutProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
return ( return (
<Link to="/" className="pf-c-button pf-m-primary" type="button" onClick={this.handleLogout}><Msg msgKey="doSignOut"/></Link> <Button onClick={handleLogout}><Msg msgKey="doSignOut"/></Button>
); );
} }
} }
interface LogoutDropdownItemProps {}
export class LogoutDropdownItem extends React.Component<LogoutDropdownItemProps> {
public render(): React.ReactNode {
return (
<DropdownItem key="logout" onClick={handleLogout}>
{Msg.localize('doSignOut')}
</DropdownItem>
);
}
}

View file

@ -30,23 +30,27 @@ export class Msg extends React.Component<MsgProps> {
} }
public render(): React.ReactNode { public render(): React.ReactNode {
let message: string = l18nMsg[this.props.msgKey]; return (
if (message === undefined) message = this.props.msgKey; <React.Fragment>{Msg.localize(this.props.msgKey, this.props.params)}</React.Fragment>
);
}
public static localize(msgKey: string, params?: string[]): string {
let message: string = l18nMsg[msgKey];
if (message === undefined) message = msgKey;
if (this.props.params !== undefined) { if (params !== undefined) {
this.props.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);
}) })
} }
return ( return message;
<span>{message}</span>
);
} }
// if the param has Freemarker syntax, try to look up its value // if the param has Freemarker syntax, try to look up its value
private processParam(param: string): string { private static processParam(param: string): string {
if (!(param.startsWith('${') && param.endsWith('}'))) return param; if (!(param.startsWith('${') && param.endsWith('}'))) return param;
// remove Freemarker syntax // remove Freemarker syntax

View file

@ -0,0 +1,47 @@
/*
* 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 {Msg} from '../widgets/Msg';
import {DropdownItem} from '@patternfly/react-core';
import {ArrowIcon} from '@patternfly/react-icons';
declare const referrerName: string;
declare const referrerUri: string;
export interface ReferrerDropdownItemProps {
}
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
export class ReferrerDropdownItem extends React.Component<ReferrerDropdownItemProps> {
public constructor(props: ReferrerDropdownItemProps) {
super(props);
}
public render(): React.ReactNode {
return (
<DropdownItem href={referrerUri}>
<ArrowIcon /> {Msg.localize('backTo', [referrerName])}
</DropdownItem>
);
}
};

View file

@ -17,29 +17,28 @@
import * as React from 'react'; import * as React from 'react';
import {Msg} from '../widgets/Msg'; import {Msg} from '../widgets/Msg';
import {ArrowIcon} from '@patternfly/react-icons';
declare const referrerName: string; declare const referrerName: string;
declare const referrerUri: string; declare const referrerUri: string;
export interface ReferrerProps { export interface ReferrerLinkProps {
} }
/** /**
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc. * @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/ */
export class Referrer extends React.Component<ReferrerProps> { export class ReferrerLink extends React.Component<ReferrerLinkProps> {
public constructor(props: ReferrerProps) { public constructor(props: ReferrerLinkProps) {
super(props); super(props);
} }
public render(): React.ReactNode { public render(): React.ReactNode {
if (typeof referrerName === "undefined") return null;
return ( return (
<a className="nav-item-iconic" href={referrerUri}> <a href={referrerUri}>
<span className="pficon-arrow"></span> <ArrowIcon/> <Msg msgKey="backTo" params={[referrerName]}/>
<Msg msgKey="backTo" params={[referrerName]}/>
</a> </a>
); );
} }

View file

@ -11,18 +11,19 @@
"author": "Stan Silvert", "author": "Stan Silvert",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@patternfly/patternfly": "^1.0.227", "@patternfly/patternfly": "^1.0.248",
"@patternfly/react-core": "^2.9.2",
"axios": "^0.18.0", "axios": "^0.18.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"react": "^16.5.2", "react": "^16.8.5",
"react-dom": "^16.5.2", "react-dom": "^16.8.5",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"systemjs": "^0.20.17", "systemjs": "^0.20.17",
"systemjs-plugin-babel": "0.0.25" "systemjs-plugin-babel": "0.0.25"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^16.4.14", "@types/react": "^16.8.8",
"@types/react-dom": "^16.0.8", "@types/react-dom": "^16.8.3",
"@types/react-router-dom": "^4.3.1", "@types/react-router-dom": "^4.3.1",
"@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",

View file

@ -18,13 +18,29 @@
'react-dom': 'npm:react-dom/umd/react-dom.development.js', 'react-dom': 'npm:react-dom/umd/react-dom.development.js',
'react-router-dom': 'npm:react-router-dom/umd/react-router-dom.js', 'react-router-dom': 'npm:react-router-dom/umd/react-router-dom.js',
'@patternfly': 'npm:@patternfly', //'@patternfly/patternfly': 'npm:@patternfly/patternfly',
'react-styles': "react-styles/loader.js", '@patternfly/patternfly': 'npm:@patternfly/react-core/dist/umd/@patternfly/patternfly',
//'patternfly-react-core-button': 'patternflyreactcore:js/components/Button/index.js', '@patternfly/react-core': 'npm:@patternfly/react-core/dist/umd/index.js',
'@patternfly/react-styles': 'npm:@patternfly/react-styles/dist/umd/index.js',
'@patternfly/react-icons': 'npm:@patternfly/react-icons/dist/umd/index.js',
'@patternfly/react-tokens': 'npm:@patternfly/react-tokens/dist/umd/index.js',
'emotion': 'npm:emotion/dist/emotion.umd.min.js',
'camel-case': 'npm:camel-case/camel-case.js',
'upper-case': 'npm:upper-case/upper-case.js',
'no-case': 'npm:no-case/no-case.js',
'lower-case': 'npm:lower-case/lower-case.js',
'prop-types': 'npm:prop-types/prop-types.min.js',
'exenv': 'npm:exenv/index.js',
'focus-trap': 'npm:focus-trap/dist/focus-trap.min.js',
'focus-trap-react': 'npm:focus-trap-react/dist/focus-trap-react.js',
'@tippy.js/react': 'npm:@tippy.js/react/dist/Tippy.min.js',
'tippy.js': 'npm:tippy.js/dist/tippy.min.js',
'moment': 'npm:moment/min/moment-with-locales.min.js', 'moment': 'npm:moment/min/moment-with-locales.min.js',
'axios': 'npm:axios/dist/axios.min.js', 'axios': 'npm:axios/dist/axios.min.js',
'history': 'npm:history/umd/history.min.js',
}, },
bundles: { bundles: {
@ -50,11 +66,99 @@
} }
} }
}, },
rxjs: { rxjs: {
defaultExtension: false defaultExtension: false
}, },
'npm:@patternfly/react-core/dist/umd/components': {
main: './index.js',
defaultExtension: 'js',
map: {
'./Alert': './Alert/index.js',
'./AboutModal': './AboutModal/index.js',
'./ApplicationLauncher': './ApplicationLauncher/index.js',
'./Avatar': './Avatar/index.js',
'./Backdrop': './Backdrop/index.js',
'./BackgroundImage': './BackgroundImage/index.js',
'./Badge': './Badge/index.js',
'./Brand': './Brand/index.js',
'./Breadcrumb': './Breadcrumb/index.js',
'./Button': './Button/index.js',
'./Card': './Card/index.js',
'./Checkbox': './Checkbox/index.js',
'./ChipGroup': './ChipGroup/index.js',
'./ContextSelector': './ContextSelector/index.js',
'./DataList': './DataList/index.js',
'./Dropdown': './Dropdown/index.js',
'./EmptyState': './EmptyState/index.js',
'./Form': './Form/index.js',
'./FormSelect': './FormSelect/index.js',
'./InputGroup': './InputGroup/index.js',
'./Label': './Label/index.js',
'./List': './List/index.js',
'./LoginPage': './LoginPage/index.js',
'./Modal': './Modal/index.js',
'./Nav': './Nav/index.js',
'./Page': './Page/index.js',
'./Popover': './Popover/index.js',
'./Progress': './Progress/index.js',
'./Pagination': './Pagination/index.js',
'./Radio': './Radio/index.js',
'./Select': './Select/index.js',
'./Switch': './Switch/index.js',
'./Tabs': './Tabs/index.js',
'./Text': './Text/index.js',
'./TextArea': './TextArea/index.js',
'./TextInput': './TextInput/index.js',
'./Title': './Title/index.js',
'./Tooltip': './Tooltip/index.js',
'./Wizard': './Wizard/index.js',
'./Bullseye': './Bullseye/index.js',
'./Gallery': './Gallery/index.js',
'./Grid': './Grid/index.js',
'./Level': './Level/index.js',
'./Split': './Split/index.js',
'./Stack': './Stack/index.js',
'./Toolbar': './Toolbar/index.js',
}
},
'npm:@patternfly/react-core/dist/umd/styles': {
main: './index.js',
defaultExtension: 'js',
},
'npm:@patternfly/react-core/dist/umd/helpers': {
main: './index.js',
defaultExtension: 'js',
},
'npm:@patternfly/react-core/dist/umd/layouts': {
main: './index.js',
defaultExtension: 'js',
map: {
'./Bullseye': './Bullseye/index.js',
'./Gallery': './Gallery/index.js',
'./Level': './Level/index.js',
'./Grid': './Grid/index.js',
'./Stack': './Stack/index.js',
'./Split': './Split/index.js',
'./Toolbar': './Toolbar/index.js',
}
},
'npm:@patternfly/react-core/dist/umd/@patternfly/patternfly/utilities/Accessibility': {
defaultExtension: 'js',
map: {
'./accessibility.css': './accessibility.css.js'
}
},
'npm:@patternfly/react-icons/dist/umd': {
main: './index.js',
defaultExtension: 'js',
},
'npm:@patternfly/react-styles/dist/umd': {
defaultExtension: 'js',
},
'npm:no-case/vendor': {
defaultExtension: 'js',
},
} }
}); });
})(this); })(this);