initial version
change favicon based on env props
This commit is contained in:
parent
92807dc128
commit
14860ae628
47 changed files with 18649 additions and 0 deletions
2
.env.dev
Normal file
2
.env.dev
Normal file
|
@ -0,0 +1,2 @@
|
|||
BACKEND_URL=http://localhost:8180/auth/admin/realms/
|
||||
SNOWPACK_PUBLIC_FAVICON=favicon.ico
|
2
.env.rh-sso
Normal file
2
.env.rh-sso
Normal 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
131
.gitignore
vendored
Normal 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
4
.storybook/main.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
stories: ['../stories/**/*.stories.js'],
|
||||
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
|
||||
};
|
35
.storybook/preview-head.html
Normal file
35
.storybook/preview-head.html
Normal 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
3
babel.config.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "@snowpack/app-scripts-react/babel.config.json"
|
||||
}
|
7
jest.config.js
Normal file
7
jest.config.js
Normal 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
5
jest.setup.js
Normal 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
55
package.json
Normal 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
5
postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import')({path: ['node_modules/@patternfly/patternfly/']}),
|
||||
]
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 627 B |
5
public/index.css
Normal file
5
public/index.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "patternfly.min.css";
|
||||
|
||||
.brand {
|
||||
height: 35px;
|
||||
}
|
42
public/index.html
Normal file
42
public/index.html
Normal 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
8
public/keycloak.json
Normal 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
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
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
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
11
snowpack.config.js
Normal file
11
snowpack.config.js
Normal 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
16
src/App.test.tsx
Normal 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
26
src/App.tsx
Normal 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
13
src/PageHeader.tsx
Normal 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
29
src/PageNav.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
1
src/__mocks__/fileMock.js
Normal file
1
src/__mocks__/fileMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = 'test-file-stub';
|
1
src/__mocks__/styleMock.js
Normal file
1
src/__mocks__/styleMock.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
6
src/auth/KeycloakContext.tsx
Normal file
6
src/auth/KeycloakContext.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { KeycloakService } from './keycloak.service';
|
||||
|
||||
export const KeycloakContext = React.createContext<KeycloakService | undefined>(
|
||||
undefined
|
||||
);
|
86
src/auth/keycloak.service.ts
Normal file
86
src/auth/keycloak.service.ts
Normal 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
11
src/auth/keycloak.ts
Normal 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;
|
||||
}
|
68
src/clients/ClientList.tsx
Normal file
68
src/clients/ClientList.tsx
Normal 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>
|
||||
);
|
||||
};
|
36
src/clients/client-model.ts
Normal file
36
src/clients/client-model.ts
Normal 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[];
|
||||
}
|
460
src/clients/mock-clients.json
Normal file
460
src/clients/mock-clients.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
33
src/components/data-loader/DataLoader.tsx
Normal file
33
src/components/data-loader/DataLoader.tsx
Normal 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>
|
||||
);
|
||||
}
|
43
src/components/realm-selector/RealmSelector.tsx
Normal file
43
src/components/realm-selector/RealmSelector.tsx
Normal 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>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
17
src/components/realm-selector/realm-selector.module.css
Normal file
17
src/components/realm-selector/realm-selector.module.css
Normal 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;
|
||||
}
|
6
src/http-service/HttpClientContext.tsx
Normal file
6
src/http-service/HttpClientContext.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { createContext } from 'react';
|
||||
import { HttpClient } from './http-client';
|
||||
|
||||
export const HttpClientContext = createContext<HttpClient | undefined>(
|
||||
undefined
|
||||
);
|
149
src/http-service/http-client.ts
Normal file
149
src/http-service/http-client.ts
Normal 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
23
src/i18n.ts
Normal 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
25
src/index.tsx
Normal 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
17
src/messages.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
14
stories/0-Welcome.stories.js
Normal file
14
stories/0-Welcome.stories.js
Normal 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',
|
||||
};
|
18
stories/1-Button.stories.js
Normal file
18
stories/1-Button.stories.js
Normal 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>
|
||||
);
|
33
stories/2-Toobar.stories.js
Normal file
33
stories/2-Toobar.stories.js
Normal 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>
|
||||
} />
|
||||
} />
|
||||
);
|
35
stories/3-DataLoader.stories.js
Normal file
35
stories/3-DataLoader.stories.js
Normal 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>
|
||||
);
|
||||
});
|
11
stories/4-ClientList.stories.js
Normal file
11
stories/4-ClientList.stories.js
Normal 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
63
tsconfig.json
Normal 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
8
types/import.d.ts
vendored
Normal 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
36
types/static.d.ts
vendored
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue