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