initial version

change favicon based on env props
This commit is contained in:
Erik Jan de Wit 2020-08-04 14:59:41 +02:00
parent 92807dc128
commit 14860ae628
47 changed files with 18649 additions and 0 deletions

2
.env.dev Normal file
View file

@ -0,0 +1,2 @@
BACKEND_URL=http://localhost:8180/auth/admin/realms/
SNOWPACK_PUBLIC_FAVICON=favicon.ico

2
.env.rh-sso Normal file
View file

@ -0,0 +1,2 @@
BACKEND_URL=http://prod-url/auth/admin/realms/
SNOWPACK_PUBLIC_FAVICON=rh-sso-favicon.ico

131
.gitignore vendored Normal file
View file

@ -0,0 +1,131 @@
# Created by https://www.gitignore.io/api/node,react
# Edit at https://www.gitignore.io/?templates=node,react
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# rollup.js default build output
dist/
lib/
# Uncomment the public line if your project uses Gatsby
# https://nextjs.org/blog/next-9-1#public-directory-support
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
# public
# Storybook build outputs
.out
.storybook-out
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Temporary folders
tmp/
temp/
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
*.sublime*
psd
thumb
sketch
# End of https://www.gitignore.io/api/node,react
# snowpack
web_modules/
build/
.build/
public/assets/

4
.storybook/main.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
stories: ['../stories/**/*.stories.js'],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};

View file

@ -0,0 +1,35 @@
<link rel="stylesheet" href="https://unpkg.com/@patternfly/patternfly@4/patternfly.css" crossorigin />
<!-- <script src="https://unpkg.com/keycloak-js@10.0.1/dist/keycloak.min.js"></script> -->
<script>
// window.onload = () => {
// keycloak = new Keycloak({
// url: "http://localhost:8180/auth/",
// realm: "master",
// clientId: 'new'
// });
// keycloak.init({ onLoad: 'check-sso', flow: 'standard', responseMode: 'fragment' }).then((authenticated) => {
// if (!authenticated) {
// keycloak.login({ redirectUri: window.location.href });
// }
// }).catch((error) => {
// console.log(error);
// alert('failed to initialize');
// });
// }
</script>
<style>
#root {
padding: 3rem;
}
.bg-checkerboard {
background-image: linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
</style>

3
babel.config.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "@snowpack/app-scripts-react/babel.config.json"
}

7
jest.config.js Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
...require("@snowpack/app-scripts-react/jest.config.js")(),
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/src/__mocks__/styleMock.js"
}
};

5
jest.setup.js Normal file
View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect";

55
package.json Normal file
View file

@ -0,0 +1,55 @@
{
"name": "keycloak-admin",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "snowpack dev",
"start:dev": "env-cmd -f .env.dev yarn start",
"start:rh-sso": "env-cmd -f .env.rh-sso yarn start",
"build": "snowpack build",
"test": "jest",
"format": "prettier --single-quote --write \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "prettier --check --single-quote \"src/**/*.{js,jsx,ts,tsx}\"",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"dependencies": {
"@patternfly/patternfly": "^4.16.7",
"@patternfly/react-core": "^4.23.1",
"@patternfly/react-icons": "^4.4.2",
"@patternfly/react-table": "^4.12.1",
"i18next": "^19.6.2",
"i18next-http-backend": "^1.0.17",
"keycloak-js": "^11.0.0",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"react-i18next": "^11.7.0"
},
"devDependencies": {
"@babel/core": "^7.10.5",
"@snowpack/app-scripts-react": "^1.4.0",
"@snowpack/plugin-parcel": "^1.3.0",
"@storybook/addon-actions": "^5.3.19",
"@storybook/addon-info": "^5.3.19",
"@storybook/addon-links": "^5.3.19",
"@storybook/addons": "^5.3.19",
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "^5.11.0",
"@testing-library/react": "^10.4.6",
"@types/dot": "^1.1.4",
"@types/jest": "^26.0.4",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"babel-loader": "^8.1.0",
"env-cmd": "^10.1.0",
"jest": "^25.4.0",
"postcss": "^7.0.32",
"postcss-cli": "^7.1.1",
"postcss-import": "^12.0.1",
"prettier": "^2.0.5",
"react-scripts": "^3.4.1",
"snowpack": "^2.6.4",
"typescript": "^3.8.3"
}
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('postcss-import')({path: ['node_modules/@patternfly/patternfly/']}),
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

5
public/index.css Normal file
View file

@ -0,0 +1,5 @@
@import "patternfly.min.css";
.brand {
height: 35px;
}

42
public/index.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link id="favicon" rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Web site to manage keycloak" />
<title>Keycloak Administration Console</title>
</head>
<body style="height: 100%;">
<div id="app" style="height: 100%">
<div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px"
height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<path d="M10 50A40 40 0 0 0 90 50A40 42 0 0 1 10 50" fill="#5DBCD2" stroke="none"
transform="rotate(16.3145 50 51)">
<animateTransform attributeName="transform" type="rotate" dur="1s" repeatCount="indefinite" keyTimes="0;1"
values="0 50 51;360 50 51"></animateTransform>
</path>
</svg>
</div>
</div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/_dist_/index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

8
public/keycloak.json Normal file
View file

@ -0,0 +1,8 @@
{
"realm": "master",
"auth-server-url": "http://localhost:8180/auth/",
"ssl-required": "external",
"resource": "new",
"public-client": true,
"confidential-port": 0
}

1
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/rh-sso-favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

11
snowpack.config.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
"extends": "@snowpack/app-scripts-react",
"scripts": {
"build:css": "postcss"
},
"proxy": {
"/realms": process.env.BACKEND_URL
},
"plugins": ["@snowpack/plugin-parcel"]
}

16
src/App.test.tsx Normal file
View file

@ -0,0 +1,16 @@
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import { render } from '@testing-library/react';
import { i18n } from './i18n';
import { App } from './App';
test('renders Welcome', () => {
const { getByText } = render(
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
);
const titleElement = getByText(/Welcome to React and react-i18next/i);
expect(titleElement).toBeInTheDocument();
});

26
src/App.tsx Normal file
View file

@ -0,0 +1,26 @@
import React, { useContext } from 'react';
import { ClientList } from './clients/ClientList';
import { DataLoader } from './components/data-loader/DataLoader';
import { HttpClientContext } from './http-service/HttpClientContext';
import { Client } from './clients/client-model';
import { Page } from '@patternfly/react-core';
import { Header } from './PageHeader';
import { PageNav } from './PageNav';
export const App = () => {
const httpClient = useContext(HttpClientContext);
const loader = async () => {
return await httpClient
?.doGet('/realms/master/clients?first=0&max=20&search=true')
.then((r) => r.data as Client[]);
};
return (
<Page header={<Header />} sidebar={<PageNav />}>
<DataLoader loader={loader}>
{(clients) => <ClientList clients={clients} />}
</DataLoader>
</Page>
);
};

13
src/PageHeader.tsx Normal file
View file

@ -0,0 +1,13 @@
import React, { useContext } from 'react';
import { PageHeader, Brand, PageHeaderTools } from '@patternfly/react-core';
import { KeycloakContext } from './auth/KeycloakContext';
export const Header = () => {
const keycloak = useContext(KeycloakContext);
return (
<PageHeader
logo={<Brand src="/logo.svg" alt="Logo" style={{ height: '35px' }} />}
headerTools={<PageHeaderTools>{keycloak?.loggedInUser}</PageHeaderTools>}
/>
);
};

29
src/PageNav.tsx Normal file
View file

@ -0,0 +1,29 @@
import React from 'react';
import { Nav, NavItem, NavList, PageSidebar } from '@patternfly/react-core';
import { RealmSelector } from './components/realm-selector/RealmSelector';
export const PageNav = () => {
return (
<PageSidebar
nav={
<Nav>
<NavList>
<RealmSelector realm="Master" realmList={['Photoz']} />
<NavItem id="default-link1" to="#default-link1" itemId={0}>
Link 1
</NavItem>
<NavItem id="default-link2" to="#default-link2" itemId={1} isActive>
Current link
</NavItem>
<NavItem id="default-link3" to="#default-link3" itemId={2}>
Link 3
</NavItem>
<NavItem id="default-link4" to="#default-link4" itemId={3}>
Link 4
</NavItem>
</NavList>
</Nav>
}
/>
);
};

View file

@ -0,0 +1 @@
module.exports = 'test-file-stub';

View file

@ -0,0 +1 @@
module.exports = {};

View file

@ -0,0 +1,6 @@
import * as React from 'react';
import { KeycloakService } from './keycloak.service';
export const KeycloakContext = React.createContext<KeycloakService | undefined>(
undefined
);

View file

@ -0,0 +1,86 @@
import { KeycloakLoginOptions } from 'keycloak-js';
import i18next from 'i18next';
import { initOptions } from '../i18n';
import { useTranslation } from 'react-i18next';
export type KeycloakClient = Keycloak.KeycloakInstance;
type Token = {
given_name: string;
family_name: string;
preferred_username: string;
};
export class KeycloakService {
private keycloakAuth: KeycloakClient;
public constructor(keycloak: KeycloakClient) {
this.keycloakAuth = keycloak;
}
public authenticated(): boolean {
return this.keycloakAuth.authenticated
? this.keycloakAuth.authenticated
: false;
}
public login(options?: KeycloakLoginOptions): void {
this.keycloakAuth.login(options);
}
public logout(redirectUri: string): void {
this.keycloakAuth.logout({ redirectUri: redirectUri });
}
public account(): void {
this.keycloakAuth.accountManagement();
}
public authServerUrl(): string | undefined {
const authServerUrl = this.keycloakAuth.authServerUrl;
return authServerUrl!.charAt(authServerUrl!.length - 1) === '/'
? authServerUrl
: authServerUrl + '/';
}
public realm(): string | undefined {
return this.keycloakAuth.realm;
}
public get loggedInUser(): string {
const { t } = useTranslation();
return this.loggedInUserName(t, this.keycloakAuth.tokenParsed as Token);
}
private loggedInUserName = (t: Function, tokenParsed: Token) => {
let userName = t('unknownUser');
if (tokenParsed) {
const givenName = tokenParsed.given_name;
const familyName = tokenParsed.family_name;
const preferredUsername = tokenParsed.preferred_username;
if (givenName && familyName) {
userName = t('fullName', { givenName, familyName });
} else {
userName = givenName || familyName || preferredUsername || userName;
}
}
return userName;
};
public getToken(): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (this.keycloakAuth.token) {
this.keycloakAuth
.updateToken(5)
.then(() => {
resolve(this.keycloakAuth.token as string);
})
.catch(() => {
reject('Failed to refresh token');
});
} else {
reject('Not logged in');
}
});
}
}

11
src/auth/keycloak.ts Normal file
View file

@ -0,0 +1,11 @@
import Keycloak, { KeycloakInstance } from 'keycloak-js';
const keycloak: KeycloakInstance = Keycloak();
export default async function (): Promise<KeycloakInstance> {
await keycloak
.init({ onLoad: 'check-sso', pkceMethod: 'S256' })
.catch(() => {
alert('failed to initialize keycloak');
});
return keycloak;
}

View file

@ -0,0 +1,68 @@
import React, { Fragment } from 'react';
import { Table, TableBody, TableHeader } from '@patternfly/react-table';
import {
ToolbarContent,
ToolbarItem,
Pagination,
Toolbar,
InputGroup,
TextInput,
Button,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import { Client } from './client-model';
type ClientListProps = {
clients?: Client[];
};
const columns: (keyof Client)[] = ['clientId', 'protocol', 'baseUrl'];
export const ClientList = ({ clients }: ClientListProps) => {
const pagination = (variant: 'top' | 'bottom' = 'top') => (
<Pagination
isCompact
itemCount={100}
page={1}
perPage={10}
variant={variant}
/>
);
const data = clients!.map((c) => {
return { cells: columns.map((col) => c[col]) };
});
return (
<Fragment>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<InputGroup>
<TextInput type="text" aria-label="search for client criteria" />
<Button variant="control" aria-label="search for client">
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
<ToolbarItem>
<Button>Create client</Button>
<Button variant="link">Import client</Button>
</ToolbarItem>
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
</ToolbarContent>
</Toolbar>
<Table
cells={['Client Id', 'Type', 'Base URL']}
rows={data}
aria-label="Client list"
>
<TableHeader />
<TableBody />
</Table>
<Toolbar>
<ToolbarItem>{pagination('bottom')}</ToolbarItem>
</Toolbar>
</Fragment>
);
};

View file

@ -0,0 +1,36 @@
export interface Client {
id: string;
clientId: string;
name: string;
description: string;
rootUrl: string;
adminUrl: string;
baseUrl: string;
surrogateAuthRequired: boolean;
enabled: boolean;
alwaysDisplayInConsole: boolean;
clientAuthenticatorType: string;
secret: string;
registrationAccessToken: string;
defaultRoles: string[];
redirectUris: string[];
webOrigins: string[];
notBefore: number;
bearerOnly: boolean;
consentRequired: boolean;
standardFlowEnabled: boolean;
implicitFlowEnabled: boolean;
directAccessGrantsEnabled: boolean;
serviceAccountsEnabled: boolean;
authorizationServicesEnabled: boolean;
publicClient: boolean;
frontchannelLogout: boolean;
protocol: string;
attributes: Map<string, string>;
authenticationFlowBindingOverrides: Map<string, string>;
fullScopeAllowed: boolean;
nodeReRegistrationTimeout: number;
registeredNodes: Map<string, number>;
//protocolMappers: ProtocolMapperRepresentation[];
}

View file

@ -0,0 +1,460 @@
[
{
"id":"767756c2-21f8-431c-9f4b-edf30654d653",
"clientId":"account",
"name":"${client_account}",
"rootUrl":"${authBaseUrl}",
"baseUrl":"/realms/master/account/",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"defaultRoles":[
"view-profile",
"manage-account"
],
"redirectUris":[
"/realms/master/account/*"
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":false,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":false,
"nodeReRegistrationTimeout":0,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"337dc87b-e08d-409e-aaac-6ab7df4b925b",
"clientId":"account-console",
"name":"${client_account-console}",
"rootUrl":"${authBaseUrl}",
"baseUrl":"/realms/master/account/",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
"/realms/master/account/*"
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":true,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
"pkce.code.challenge.method":"S256"
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":false,
"nodeReRegistrationTimeout":0,
"protocolMappers":[
{
"id":"204da5b5-40d8-4a2a-b864-0947a9bcd21d",
"name":"audience resolve",
"protocol":"openid-connect",
"protocolMapper":"oidc-audience-resolve-mapper",
"consentRequired":false,
"config":{
}
}
],
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"60d59afe-7926-4c22-b829-798125793ef5",
"clientId":"admin-cli",
"name":"${client_admin-cli}",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":false,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":true,
"serviceAccountsEnabled":false,
"publicClient":true,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":false,
"nodeReRegistrationTimeout":0,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"c2d74093-2b8c-4ecb-870f-c7358ff48237",
"clientId":"broker",
"name":"${client_broker}",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":false,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":false,
"nodeReRegistrationTimeout":0,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"66135023-e667-4864-b1f3-f87e805fabc2",
"clientId":"master-realm",
"name":"master Realm",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":true,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":false,
"frontchannelLogout":false,
"attributes":{
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":true,
"nodeReRegistrationTimeout":0,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"324f4182-d302-44f8-ac8a-149eaa29dc90",
"clientId":"new",
"rootUrl":"http://localhost:8080/",
"adminUrl":"http://localhost:8080/",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
"http://localhost:8080/*"
],
"webOrigins":[
"+"
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":true,
"serviceAccountsEnabled":false,
"publicClient":true,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
"saml.assertion.signature":"false",
"saml.force.post.binding":"false",
"saml.multivalued.roles":"false",
"saml.encrypt":"false",
"saml.server.signature":"false",
"saml.server.signature.keyinfo.ext":"false",
"exclude.session.state.from.auth.response":"false",
"saml_force_name_id_format":"false",
"saml.client.signature":"false",
"tls.client.certificate.bound.access.tokens":"false",
"saml.authnstatement":"false",
"display.on.consent.screen":"false",
"saml.onetimeuse.condition":"false"
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":true,
"nodeReRegistrationTimeout":-1,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"fb45882b-4d85-4f40-920e-6a68298d36d0",
"clientId":"photoz-realm",
"name":"photoz Realm",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
],
"webOrigins":[
],
"notBefore":0,
"bearerOnly":true,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":false,
"frontchannelLogout":false,
"attributes":{
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":true,
"nodeReRegistrationTimeout":0,
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
},
{
"id":"9ed60e41-d794-4046-842f-3247bf32f5ce",
"clientId":"security-admin-console",
"name":"${client_security-admin-console}",
"rootUrl":"${authAdminUrl}",
"baseUrl":"/admin/master/console/",
"surrogateAuthRequired":false,
"enabled":true,
"alwaysDisplayInConsole":false,
"clientAuthenticatorType":"client-secret",
"redirectUris":[
"/admin/master/console/*"
],
"webOrigins":[
"+"
],
"notBefore":0,
"bearerOnly":false,
"consentRequired":false,
"standardFlowEnabled":true,
"implicitFlowEnabled":false,
"directAccessGrantsEnabled":false,
"serviceAccountsEnabled":false,
"publicClient":true,
"frontchannelLogout":false,
"protocol":"openid-connect",
"attributes":{
"pkce.code.challenge.method":"S256"
},
"authenticationFlowBindingOverrides":{
},
"fullScopeAllowed":false,
"nodeReRegistrationTimeout":0,
"protocolMappers":[
{
"id":"b1df1298-af87-4557-bc9f-b4119ba1d01d",
"name":"locale",
"protocol":"openid-connect",
"protocolMapper":"oidc-usermodel-attribute-mapper",
"consentRequired":false,
"config":{
"userinfo.token.claim":"true",
"user.attribute":"locale",
"id.token.claim":"true",
"access.token.claim":"true",
"claim.name":"locale",
"jsonType.label":"String"
}
}
],
"defaultClientScopes":[
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes":[
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access":{
"view":true,
"configure":true,
"manage":true
}
}
]

View file

@ -0,0 +1,33 @@
import React, { useEffect, useState } from 'react';
import { Spinner } from '@patternfly/react-core';
type DataLoaderProps<T> = {
loader: () => Promise<T>;
deps?: any[];
children: ((arg: T) => any) | React.ReactNode;
};
export function DataLoader<T>(props: DataLoaderProps<T>) {
const [data, setData] = useState<{ result: T } | undefined>(undefined);
useEffect(() => {
setData(undefined);
const loadData = async () => {
const result = await props.loader();
setData({ result });
};
loadData();
}, [props]);
if (!!data) {
if (props.children instanceof Function) {
return props.children(data.result);
}
return props.children;
}
return (
<div style={{ textAlign: 'center' }}>
<Spinner />
</div>
);
}

View file

@ -0,0 +1,43 @@
import React, { useState } from 'react';
import {
Dropdown,
DropdownToggle,
DropdownItem,
Button,
} from '@patternfly/react-core';
import style from './realm-selector.module.css';
type RealmSelectorProps = {
realm: string;
realmList: string[];
};
export const RealmSelector = ({ realm, realmList }: RealmSelectorProps) => {
const [open, setOpen] = useState(false);
const dropdownItems = realmList.map((r) => (
<DropdownItem key={r}>{r}</DropdownItem>
));
return (
<Dropdown
id="realm-select"
className={style.dropdown}
isOpen={open}
toggle={
<DropdownToggle
id="realm-select-toggle"
onToggle={() => setOpen(!open)}
className={style.toggle}
>
{realm}
</DropdownToggle>
}
dropdownItems={[
...dropdownItems,
<DropdownItem key="add">
<Button isBlock>Add Realm</Button>
</DropdownItem>,
]}
/>
);
};

View file

@ -0,0 +1,17 @@
.dropdown {
width: 100%;
border-bottom: 1px solid var(--pf-c-nav__link--before--BorderColor);
}
.toggle {
color: var(--pf-c-nav__link--m-current--Color);
}
.toggle:focus {
outline:0;
}
.toggle::before {
border: none;
}

View file

@ -0,0 +1,6 @@
import { createContext } from 'react';
import { HttpClient } from './http-client';
export const HttpClientContext = createContext<HttpClient | undefined>(
undefined
);

View file

@ -0,0 +1,149 @@
import { KeycloakService } from '../auth/keycloak.service';
type ConfigResolve = (config: RequestInit) => void;
export interface HttpResponse<T = {}> extends Response {
data?: T;
}
export interface RequestInitWithParams extends RequestInit {
params?: { [name: string]: string | number };
}
export class AccountServiceError extends Error {
constructor(public response: HttpResponse) {
super(response.statusText);
}
}
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
export class HttpClient {
private kcSvc: KeycloakService;
public constructor(keycloakService: KeycloakService) {
this.kcSvc = keycloakService;
}
public async doGet<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: 'get' });
}
public async doDelete<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: 'delete' });
}
public async doPost<T>(
endpoint: string,
body: string | {},
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: 'post',
});
}
public async doPut<T>(
endpoint: string,
body: string | {},
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: 'put',
});
}
public async doRequest<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
const response: HttpResponse<T> = await fetch(
this.makeUrl(endpoint, config).toString(),
await this.makeConfig(config)
);
try {
response.data = await response.json();
} catch (e) {} // ignore. Might be empty
if (!response.ok) {
this.handleError(response);
throw new AccountServiceError(response);
}
return response;
}
private handleError(response: HttpResponse): void {
if (response != null && response.status === 401) {
// session timed out?
this.kcSvc.login();
}
if (response != null && response.data != null) {
console.error(response.data);
// ContentAlert.danger(
// `${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`
// );
// } else {
// ContentAlert.danger(response.statusText);
}
}
private makeUrl(url: string, config?: RequestInitWithParams): string {
const searchParams = new URLSearchParams();
// add request params
if (config && config.hasOwnProperty('params')) {
const params: { [name: string]: string } = (config.params as {}) || {};
Object.keys(params).forEach((key) =>
searchParams.append(key, params[key])
);
}
return url + searchParams.toString();
}
private makeConfig(config: RequestInit = {}): Promise<RequestInit> {
return new Promise((resolve: ConfigResolve) => {
this.kcSvc
.getToken()
.then((token: string) => {
resolve({
...config,
headers: {
'Content-Type': 'application/json',
...config.headers,
Authorization: 'Bearer ' + token,
},
});
})
.catch(() => {
this.kcSvc.login();
});
});
}
}
window.addEventListener(
'unhandledrejection',
(event: PromiseRejectionEvent) => {
event.promise.catch((error) => {
if (error instanceof AccountServiceError) {
// We already handled the error. Ignore unhandled rejection.
event.preventDefault();
}
});
}
);

23
src/i18n.ts Normal file
View file

@ -0,0 +1,23 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// import backend from "i18next-http-backend";
import messages from './messages.json';
const initOptions = {
resources: messages,
lng: 'en',
fallbackLng: 'en',
saveMissing: true,
interpolation: {
escapeValue: false,
},
};
i18n
.use(initReactI18next)
// .use(backend)
.init(initOptions);
export { i18n, initOptions };

25
src/index.tsx Normal file
View file

@ -0,0 +1,25 @@
import React from 'react';
import ReactDom from 'react-dom';
import { App } from './App';
import init from './auth/keycloak';
import { KeycloakContext } from './auth/KeycloakContext';
import { KeycloakService } from './auth/keycloak.service';
import { HttpClientContext } from './http-service/HttpClientContext';
import { HttpClient } from './http-service/http-client';
init().then((keycloak) => {
const keycloakService = new KeycloakService(keycloak);
ReactDom.render(
<KeycloakContext.Provider value={keycloakService}>
<HttpClientContext.Provider value={new HttpClient(keycloakService)}>
<App />
</HttpClientContext.Provider>
</KeycloakContext.Provider>,
document.getElementById('app')
);
});
(document.getElementById('favicon') as HTMLAnchorElement).href = `${
import.meta.env.SNOWPACK_PUBLIC_FAVICON
}`;

17
src/messages.json Normal file
View file

@ -0,0 +1,17 @@
{
"en": {
"translation": {
"personalInfoHtmlTitle": "Personal Info",
"personalInfoIntroMessage": "Manage your basic information",
"Account Security": "Account Security",
"accountSecurityIntroMessage": "Control your password and account access",
"signingIn": "Signing In",
"device-activity": "Device Activity",
"applications": "Applications",
"applicationsIntroMessage": "Track and manage your app permission to access your account",
"fullName": "{{givenName}} {{familyName}}",
"unknownUser": "Anonymous",
"Keycloak Administration Console": "RH-SSO Administration Console"
}
}
}

View file

@ -0,0 +1,14 @@
import React from 'react';
import { linkTo } from '@storybook/addon-links';
import { Welcome } from '@storybook/react/demo';
export default {
title: 'Welcome',
component: Welcome,
};
export const ToStorybook = () => <Welcome showApp={linkTo('Button')} />;
ToStorybook.story = {
name: 'to Storybook',
};

View file

@ -0,0 +1,18 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { Button } from '@storybook/react/demo';
export default {
title: 'Button',
component: Button,
};
export const Text = () => <Button onClick={action('clicked')}>Hello Button</Button>;
export const Emoji = () => (
<Button onClick={action('clicked')}>
<span role="img" aria-label="so cool">
😀 😎 👍 💯
</span>
</Button>
);

View file

@ -0,0 +1,33 @@
import React from 'react';
import { Nav, NavItem, NavList, PageSidebar, Page } from '@patternfly/react-core';
import { RealmSelector } from '../src/components/realm-selector/RealmSelector';
export default {
title: 'Toolbar'
};
export const RealmSelect = () => (
<Page sidebar={
<PageSidebar nav={
<Nav>
<NavList>
<RealmSelector realm="Master" realmList={["Master", "Photoz"]} />
<NavItem id="default-link1" to="#default-link1" itemId={0}>
Link 1
</NavItem>
<NavItem id="default-link2" to="#default-link2" itemId={1} isActive>
Current link
</NavItem>
<NavItem id="default-link3" to="#default-link3" itemId={2}>
Link 3
</NavItem>
<NavItem id="default-link4" to="#default-link4" itemId={3}>
Link 4
</NavItem>
</NavList>
</Nav>
} />
} />
);

View file

@ -0,0 +1,35 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { DataLoader } from '../src/components/data-loader/DataLoader';
storiesOf('DataLoader', module)
.add('load posts', () => {
function PostLoader(props) {
const loader = async () => {
const wait = (ms, value) => new Promise(resolve => setTimeout(resolve, ms, value))
return await fetch(props.url).then(res => res.json()).then(value => wait(3000, value));
}
return <DataLoader loader={loader}>{props.children}</DataLoader>;
}
return (
<PostLoader url="https://jsonplaceholder.typicode.com/posts">
{posts => (
<table>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
{posts.map((post, i) => (
<tr key={i}>
<td>{post.title}</td>
<td>{post.body}</td>
</tr>
))}
</table>
)}
</PostLoader>
);
});

View file

@ -0,0 +1,11 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { ClientList } from '../src/clients/ClientList';
import clientMock from '../src/clients/mock-clients.json';
storiesOf('Client list page', module)
.add('view', () => {
return (<ClientList clients={clientMock} />
);
})

63
tsconfig.json Normal file
View file

@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "lib", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
// "declarationDir": "lib" /* Output directory for generated declaration files. */
"skipLibCheck": true,
}
}

8
types/import.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
// ESM-HMR Interface: `import.meta.hot`
interface ImportMeta {
// TODO: Import the exact .d.ts files from "esm-hmr"
// https://github.com/pikapkg/esm-hmr
hot: any;
env: Record<string, any>;
}

36
types/static.d.ts vendored Normal file
View file

@ -0,0 +1,36 @@
/* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */
declare module '*.css';
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
declare module '*.webp' {
const ref: string;
export default ref;
}
declare module '*.json' {
const value: any;
export default value;
}

17046
yarn.lock Normal file

File diff suppressed because it is too large Load diff