web: re-format with prettier
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
77ed25ae34
commit
2c60ec50be
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/env",
|
||||
"@babel/typescript"
|
||||
],
|
||||
"presets": ["@babel/env", "@babel/typescript"],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
||||
[
|
||||
|
|
|
@ -14,11 +14,7 @@
|
|||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"lit",
|
||||
"custom-elements"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "lit", "custom-elements"],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
# don't lint generated code
|
||||
api/
|
||||
azure-pipelines.yml
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 100,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "consistent",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
|
@ -49,6 +49,28 @@ stages:
|
|||
command: 'custom'
|
||||
workingDir: 'web/'
|
||||
customCommand: 'run lint'
|
||||
- job: prettier
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '16.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'ts_api_client'
|
||||
path: "web/api/"
|
||||
- task: Npm@1
|
||||
inputs:
|
||||
command: 'install'
|
||||
workingDir: 'web/'
|
||||
- task: Npm@1
|
||||
inputs:
|
||||
command: 'custom'
|
||||
workingDir: 'web/'
|
||||
customCommand: 'run prettier-check'
|
||||
- job: lit_analyse
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"lit-element": "^2.5.1",
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"prettier": "^2.3.2",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.55.1",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
|
@ -66,8 +67,8 @@
|
|||
},
|
||||
"api": {
|
||||
"name": "authentik-api",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^3.9.5"
|
||||
}
|
||||
},
|
||||
|
@ -75,6 +76,7 @@
|
|||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -6405,6 +6407,17 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
||||
|
@ -10217,7 +10230,8 @@
|
|||
"typescript": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w=="
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -12932,6 +12946,11 @@
|
|||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
"build": "lingui compile && rollup -c ./rollup.config.js",
|
||||
"watch": "lingui compile && rollup -c -w",
|
||||
"lint": "eslint . --max-warnings 0 --fix",
|
||||
"lit-analyse": "lit-analyzer src"
|
||||
"lit-analyse": "lit-analyzer src",
|
||||
"prettier-check": "prettier --check .",
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"lingui": {
|
||||
"sourceLocale": "en",
|
||||
|
@ -76,6 +78,7 @@
|
|||
"lit-element": "^2.5.1",
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"prettier": "^2.3.2",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.55.1",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
|
|
|
@ -8,21 +8,37 @@ import copy from "rollup-plugin-copy";
|
|||
import babel from "@rollup/plugin-babel";
|
||||
import replace from "@rollup/plugin-replace";
|
||||
|
||||
const extensions = [
|
||||
".js", ".jsx", ".ts", ".tsx",
|
||||
];
|
||||
const extensions = [".js", ".jsx", ".ts", ".tsx"];
|
||||
|
||||
const resources = [
|
||||
{ src: "node_modules/rapidoc/dist/rapidoc-min.js", dest: "dist/" },
|
||||
|
||||
{ src: "node_modules/@patternfly/patternfly/patternfly.min.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/patternfly-base.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/Page/page.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/EmptyState/empty-state.css", dest: "dist/" },
|
||||
{ src: "node_modules/@patternfly/patternfly/components/Spinner/spinner.css", dest: "dist/" },
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/patternfly.min.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/patternfly-base.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/components/Page/page.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/components/EmptyState/empty-state.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/components/Spinner/spinner.css",
|
||||
dest: "dist/",
|
||||
},
|
||||
{ src: "src/authentik.css", dest: "dist/" },
|
||||
|
||||
{ src: "node_modules/@patternfly/patternfly/assets/*", dest: "dist/assets/" },
|
||||
{
|
||||
src: "node_modules/@patternfly/patternfly/assets/*",
|
||||
dest: "dist/assets/",
|
||||
},
|
||||
{ src: "src/assets/*", dest: "dist/assets" },
|
||||
{ src: "./icons/*", dest: "dist/assets/icons" },
|
||||
];
|
||||
|
@ -60,15 +76,15 @@ export default [
|
|||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
"declaration": true,
|
||||
"outDir": "./api/dist/",
|
||||
declaration: true,
|
||||
outDir: "./api/dist/",
|
||||
}),
|
||||
isProdBuild && terser(),
|
||||
copy({
|
||||
targets: [...resources],
|
||||
copyOnce: false,
|
||||
}),
|
||||
].filter(p => p),
|
||||
].filter((p) => p),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
|
@ -81,14 +97,14 @@ export default [
|
|||
format: "iife",
|
||||
file: "dist/poly.js",
|
||||
sourcemap: true,
|
||||
}
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
cssimport(),
|
||||
resolve({ browser: true }),
|
||||
commonjs(),
|
||||
isProdBuild && terser(),
|
||||
].filter(p => p),
|
||||
].filter((p) => p),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
|
@ -102,7 +118,7 @@ export default [
|
|||
dir: "dist",
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
chunkFileNames: "admin-[name].js"
|
||||
chunkFileNames: "admin-[name].js",
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
|
@ -116,11 +132,11 @@ export default [
|
|||
}),
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||
preventAssignment: true
|
||||
"preventAssignment": true,
|
||||
}),
|
||||
sourcemaps(),
|
||||
isProdBuild && terser(),
|
||||
].filter(p => p),
|
||||
].filter((p) => p),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
|
@ -134,7 +150,7 @@ export default [
|
|||
dir: "dist",
|
||||
sourcemap: true,
|
||||
manualChunks: manualChunks,
|
||||
chunkFileNames: "flow-[name].js"
|
||||
chunkFileNames: "flow-[name].js",
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
|
@ -148,11 +164,11 @@ export default [
|
|||
}),
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||
preventAssignment: true
|
||||
"preventAssignment": true,
|
||||
}),
|
||||
sourcemaps(),
|
||||
isProdBuild && terser(),
|
||||
].filter(p => p),
|
||||
].filter((p) => p),
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
|
|
|
@ -169,7 +169,9 @@ body {
|
|||
color: var(--ak-dark-foreground) !important;
|
||||
}
|
||||
.pf-c-table__expandable-row.pf-m-expanded {
|
||||
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(--ak-dark-background-lighter);
|
||||
--pf-c-table__expandable-row--m-expanded--BorderBottomColor: var(
|
||||
--ak-dark-background-lighter
|
||||
);
|
||||
}
|
||||
/* tabs */
|
||||
.pf-c-tabs {
|
||||
|
@ -214,7 +216,8 @@ body {
|
|||
border-bottom: 0;
|
||||
}
|
||||
/* inputs */
|
||||
optgroup, option {
|
||||
optgroup,
|
||||
option {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-input-group {
|
||||
|
@ -235,7 +238,10 @@ body {
|
|||
background-color: var(--ak-dark-background-light);
|
||||
}
|
||||
.pf-c-button.pf-m-control {
|
||||
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter) var(--ak-dark-background-lighter) var(--pf-c-button--m-control--after--BorderBottomColor) var(--ak-dark-background-lighter);
|
||||
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter)
|
||||
var(--ak-dark-background-lighter)
|
||||
var(--pf-c-button--m-control--after--BorderBottomColor)
|
||||
var(--ak-dark-background-lighter);
|
||||
background-color: var(--ak-dark-background-light);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ export interface WSMessage {
|
|||
}
|
||||
|
||||
export class WebsocketClient {
|
||||
|
||||
messageSocket?: WebSocket;
|
||||
retryDelay = 200;
|
||||
|
||||
|
@ -22,8 +21,9 @@ export class WebsocketClient {
|
|||
|
||||
connect(): void {
|
||||
if (navigator.webdriver) return;
|
||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${window.location.host
|
||||
}/ws/client/`;
|
||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
|
||||
window.location.host
|
||||
}/ws/client/`;
|
||||
this.messageSocket = new WebSocket(wsUrl);
|
||||
this.messageSocket.addEventListener("open", () => {
|
||||
console.debug(`authentik/ws: connected to ${wsUrl}`);
|
||||
|
@ -34,7 +34,7 @@ export class WebsocketClient {
|
|||
if (this.retryDelay > 3000) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`Connection error, reconnecting...`
|
||||
message: t`Connection error, reconnecting...`,
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
|
@ -50,12 +50,11 @@ export class WebsocketClient {
|
|||
bubbles: true,
|
||||
composed: true,
|
||||
detail: data as WSMessage,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
this.messageSocket.addEventListener("error", () => {
|
||||
this.retryDelay = this.retryDelay * 2;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
import CodeMirror from "codemirror";
|
||||
import "codemirror/addon/display/autorefresh";
|
||||
|
@ -13,13 +21,13 @@ import "codemirror/mode/python/python.js";
|
|||
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
|
||||
import CodeMirrorTheme from "codemirror/theme/monokai.css";
|
||||
import CodeMirrorDialogStyle from "codemirror/addon/dialog/dialog.css";
|
||||
import CodeMirrorShowHintStyle from "codemirror/addon/hint/show-hint.css";
|
||||
import CodeMirrorShowHintStyle from "codemirror/addon/hint/show-hint.css";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import YAML from "yaml";
|
||||
|
||||
@customElement("ak-codemirror")
|
||||
export class CodeMirrorTextarea extends LitElement {
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
readOnly = false;
|
||||
|
||||
@property()
|
||||
|
@ -83,11 +91,17 @@ export class CodeMirrorTextarea extends LitElement {
|
|||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [CodeMirrorStyle, CodeMirrorTheme, CodeMirrorDialogStyle, CodeMirrorShowHintStyle, css`
|
||||
.CodeMirror-wrap pre {
|
||||
word-break: break-word !important;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
CodeMirrorStyle,
|
||||
CodeMirrorTheme,
|
||||
CodeMirrorDialogStyle,
|
||||
CodeMirrorShowHintStyle,
|
||||
css`
|
||||
.CodeMirror-wrap pre {
|
||||
word-break: break-word !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
|
@ -102,7 +116,7 @@ export class CodeMirrorTextarea extends LitElement {
|
|||
readOnly: this.readOnly,
|
||||
autoRefresh: true,
|
||||
lineWrapping: true,
|
||||
value: this._value
|
||||
value: this._value,
|
||||
});
|
||||
this.editor.on("blur", () => {
|
||||
this.editor?.save();
|
||||
|
|
|
@ -4,34 +4,36 @@ import AKGlobal from "../authentik.css";
|
|||
|
||||
@customElement("ak-divider")
|
||||
export class Divider extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, AKGlobal, css`
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
return [
|
||||
PFBase,
|
||||
AKGlobal,
|
||||
css`
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.separator::before,
|
||||
.separator::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--pf-global--Color--100);
|
||||
}
|
||||
.separator::before,
|
||||
.separator::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--pf-global--Color--100);
|
||||
}
|
||||
|
||||
.separator:not(:empty)::before {
|
||||
margin-right: .25em;
|
||||
}
|
||||
.separator:not(:empty)::before {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.separator:not(:empty)::after {
|
||||
margin-left: .25em;
|
||||
}
|
||||
`];
|
||||
.separator:not(:empty)::after {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="separator"><slot></slot></div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,14 +8,13 @@ import { PFSize } from "./Spinner";
|
|||
|
||||
@customElement("ak-empty-state")
|
||||
export class EmptyState extends LitElement {
|
||||
|
||||
@property({type: String})
|
||||
@property({ type: String })
|
||||
icon = "";
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
loading = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
fullHeight = false;
|
||||
|
||||
@property()
|
||||
|
@ -28,14 +27,16 @@ export class EmptyState extends LitElement {
|
|||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}">
|
||||
<div class="pf-c-empty-state__content">
|
||||
${this.loading ?
|
||||
html`<div class="pf-c-empty-state__icon">
|
||||
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
||||
</div>`:
|
||||
html`<i class="pf-icon fa ${this.icon || "fa-question-circle"} pf-c-empty-state__icon" aria-hidden="true"></i>`}
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
${this.header}
|
||||
</h1>
|
||||
${this.loading
|
||||
? html`<div class="pf-c-empty-state__icon">
|
||||
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
||||
</div>`
|
||||
: html`<i
|
||||
class="pf-icon fa ${this.icon ||
|
||||
"fa-question-circle"} pf-c-empty-state__icon"
|
||||
aria-hidden="true"
|
||||
></i>`}
|
||||
<h1 class="pf-c-title pf-m-lg">${this.header}</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
|
@ -45,5 +46,4 @@ export class EmptyState extends LitElement {
|
|||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import PFExpandableSection from "../../node_modules/@patternfly/patternfly/compo
|
|||
|
||||
@customElement("ak-expand")
|
||||
export class Expand extends LitElement {
|
||||
|
||||
@property({ type: Boolean })
|
||||
expanded = false;
|
||||
|
||||
|
@ -20,16 +19,22 @@ export class Expand extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-expandable-section ${this.expanded ? "pf-m-expanded" : ""}">
|
||||
<button type="button" class="pf-c-expandable-section__toggle" aria-expanded="${this.expanded}" @click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="pf-c-expandable-section__toggle"
|
||||
aria-expanded="${this.expanded}"
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-expandable-section__toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="pf-c-expandable-section__toggle-text">${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span>
|
||||
<span class="pf-c-expandable-section__toggle-text"
|
||||
>${this.expanded ? t`${this.textOpen}` : t`${this.textClosed}`}</span
|
||||
>
|
||||
</button>
|
||||
<slot ?hidden=${!this.expanded} class="pf-c-expandable-section__content"></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ export enum PFColor {
|
|||
|
||||
@customElement("ak-label")
|
||||
export class Label extends LitElement {
|
||||
|
||||
@property()
|
||||
color: PFColor = PFColor.Grey;
|
||||
|
||||
|
@ -45,11 +44,14 @@ export class Label extends LitElement {
|
|||
return html`<span class="pf-c-label ${this.color}">
|
||||
<span class="pf-c-label__content">
|
||||
<span class="pf-c-label__icon">
|
||||
<i class="fas ${this.text ? "fa-fw" : ""} ${this.icon || this.getDefaultIcon()}" aria-hidden="true"></i>
|
||||
<i
|
||||
class="fas ${this.text ? "fa-fw" : ""} ${this.icon ||
|
||||
this.getDefaultIcon()}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
${this.text || ""}
|
||||
</span>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import AKGlobal from "../authentik.css";
|
||||
|
@ -10,19 +18,18 @@ import { EventsApi } from "../../api/dist";
|
|||
|
||||
@customElement("ak-page-header")
|
||||
export class PageHeader extends LitElement {
|
||||
|
||||
@property()
|
||||
icon?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
iconImage = false
|
||||
@property({ type: Boolean })
|
||||
iconImage = false;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
hasNotifications = false;
|
||||
|
||||
@property()
|
||||
set header(value: string) {
|
||||
tenant().then(tenant => {
|
||||
tenant().then((tenant) => {
|
||||
if (value !== "") {
|
||||
document.title = `${value} - ${tenant.brandingTitle}`;
|
||||
} else {
|
||||
|
@ -42,33 +49,40 @@ export class PageHeader extends LitElement {
|
|||
_header = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFButton, PFPage, PFContent, AKGlobal, css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 114px;
|
||||
}
|
||||
.pf-c-button.pf-m-plain {
|
||||
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.pf-c-page__main-section {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
font-size: 24px;
|
||||
}
|
||||
.notification-trigger.has-notifications {
|
||||
color: #2B9AF3;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFPage,
|
||||
PFContent,
|
||||
AKGlobal,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 114px;
|
||||
}
|
||||
.pf-c-button.pf-m-plain {
|
||||
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.pf-c-page__main-section {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
font-size: 24px;
|
||||
}
|
||||
.notification-trigger.has-notifications {
|
||||
color: #2b9af3;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
renderIcon(): TemplateResult {
|
||||
|
@ -82,50 +96,51 @@ export class PageHeader extends LitElement {
|
|||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
pageSize: 1,
|
||||
}).then(r => {
|
||||
this.hasNotifications = r.pagination.count > 0;
|
||||
});
|
||||
new EventsApi(DEFAULT_CONFIG)
|
||||
.eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
pageSize: 1,
|
||||
})
|
||||
.then((r) => {
|
||||
this.hasNotifications = r.pagination.count > 0;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<button
|
||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
${this.renderIcon()}
|
||||
${this.header}
|
||||
</h1>
|
||||
${this.description ?
|
||||
html`<p>${this.description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications ? "has-notifications" : ""}"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<i class="fas fa-bell"></i>
|
||||
</button>`;
|
||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>${this.renderIcon()} ${this.header}</h1>
|
||||
${this.description ? html`<p>${this.description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications
|
||||
? "has-notifications"
|
||||
: ""}"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-bell"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@ export class Spinner extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html`<span
|
||||
class="pf-c-spinner ${this.size.toString()}"
|
||||
role="progressbar"
|
||||
aria-valuetext="${t`Loading...`}">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>`;
|
||||
class="pf-c-spinner ${this.size.toString()}"
|
||||
role="progressbar"
|
||||
aria-valuetext="${t`Loading...`}"
|
||||
>
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { LitElement, html, customElement, property, CSSResult, TemplateResult, css } from "lit-element";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
|
||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
@ -11,24 +19,29 @@ export class Tabs extends LitElement {
|
|||
@property()
|
||||
currentPage?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
vertical = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFGlobal, PFTabs, AKGlobal, css`
|
||||
::slotted(*) {
|
||||
flex-grow: 2;
|
||||
}
|
||||
:host([vertical]) {
|
||||
display: flex;
|
||||
}
|
||||
:host([vertical]) .pf-c-tabs {
|
||||
width: auto !important;
|
||||
}
|
||||
:host([vertical]) .pf-c-tabs__list {
|
||||
height: 100%;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFGlobal,
|
||||
PFTabs,
|
||||
AKGlobal,
|
||||
css`
|
||||
::slotted(*) {
|
||||
flex-grow: 2;
|
||||
}
|
||||
:host([vertical]) {
|
||||
display: flex;
|
||||
}
|
||||
:host([vertical]) .pf-c-tabs {
|
||||
width: auto !important;
|
||||
}
|
||||
:host([vertical]) .pf-c-tabs__list {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
observer: MutationObserver;
|
||||
|
@ -42,7 +55,11 @@ export class Tabs extends LitElement {
|
|||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.observer.observe(this, { attributes: true, childList: true, subtree: true });
|
||||
this.observer.observe(this, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
|
@ -61,9 +78,7 @@ export class Tabs extends LitElement {
|
|||
const slot = page.attributes.getNamedItem("slot")?.value;
|
||||
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
|
||||
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
|
||||
<span class="pf-c-tabs__item-text">
|
||||
${page.getAttribute("data-tab-title")}
|
||||
</span>
|
||||
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
|
||||
</button>
|
||||
</li>`;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@ import { MessageLevel } from "../messages/Message";
|
|||
|
||||
@customElement("ak-action-button")
|
||||
export class ActionButton extends SpinnerButton {
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
apiRequest: () => Promise<any> = () => { throw new Error(); };
|
||||
apiRequest: () => Promise<any> = () => {
|
||||
throw new Error();
|
||||
};
|
||||
|
||||
callAction = (): Promise<void> => {
|
||||
this.setLoading();
|
||||
|
@ -16,13 +17,13 @@ export class ActionButton extends SpinnerButton {
|
|||
if (e instanceof Error) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: e.toString()
|
||||
message: e.toString(),
|
||||
});
|
||||
} else {
|
||||
e.text().then(t => {
|
||||
e.text().then((t) => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t
|
||||
message: t,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css";
|
||||
|
@ -35,11 +43,25 @@ export class ModalButton extends LitElement {
|
|||
@property()
|
||||
size: PFSize = PFSize.Large;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFButton, PFModalBox, PFForm, PFTitle, PFFormControl, PFBullseye, PFBackdrop, PFPage, PFCard, PFContent, AKGlobal, MODAL_BUTTON_STYLES];
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFModalBox,
|
||||
PFForm,
|
||||
PFTitle,
|
||||
PFFormControl,
|
||||
PFBullseye,
|
||||
PFBackdrop,
|
||||
PFPage,
|
||||
PFCard,
|
||||
PFContent,
|
||||
AKGlobal,
|
||||
MODAL_BUTTON_STYLES,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -53,7 +75,7 @@ export class ModalButton extends LitElement {
|
|||
}
|
||||
|
||||
resetForms(): void {
|
||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
|
||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
|
||||
if ("resetForm" in form) {
|
||||
form?.resetForm();
|
||||
}
|
||||
|
@ -62,7 +84,7 @@ export class ModalButton extends LitElement {
|
|||
|
||||
onClick(): void {
|
||||
this.open = true;
|
||||
this.querySelectorAll("*").forEach(child => {
|
||||
this.querySelectorAll("*").forEach((child) => {
|
||||
if ("requestUpdate" in child) {
|
||||
(child as LitElement).requestUpdate();
|
||||
}
|
||||
|
@ -70,17 +92,13 @@ export class ModalButton extends LitElement {
|
|||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<slot name='modal'></slot>`;
|
||||
return html`<slot name="modal"></slot>`;
|
||||
}
|
||||
|
||||
renderModal(): TemplateResult {
|
||||
return html`<div class="pf-c-backdrop">
|
||||
<div class="pf-l-bullseye">
|
||||
<div
|
||||
class="pf-c-modal-box ${this.size}"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
|
||||
<button
|
||||
@click=${() => (this.open = false)}
|
||||
class="pf-c-button pf-m-plain"
|
||||
|
@ -99,5 +117,4 @@ export class ModalButton extends LitElement {
|
|||
return html` <slot name="trigger" @click=${() => this.onClick()}></slot>
|
||||
${this.open ? this.renderModal() : ""}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
|
||||
|
@ -8,7 +16,7 @@ import { ERROR_CLASS, PRIMARY_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "../..
|
|||
|
||||
@customElement("ak-spinner-button")
|
||||
export class SpinnerButton extends LitElement {
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
isRunning = false;
|
||||
|
||||
@property()
|
||||
|
@ -60,17 +68,20 @@ export class SpinnerButton extends LitElement {
|
|||
}
|
||||
this.setLoading();
|
||||
if (this.callAction) {
|
||||
this.callAction().then(() => {
|
||||
this.setDone(SUCCESS_CLASS);
|
||||
}).catch(() => {
|
||||
this.setDone(ERROR_CLASS);
|
||||
});
|
||||
this.callAction()
|
||||
.then(() => {
|
||||
this.setDone(SUCCESS_CLASS);
|
||||
})
|
||||
.catch(() => {
|
||||
this.setDone(ERROR_CLASS);
|
||||
});
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
${this.isRunning
|
||||
? html` <span class="pf-c-button__progress">
|
||||
<ak-spinner size=${PFSize.Medium}></ak-spinner>
|
||||
</span>`
|
||||
<ak-spinner size=${PFSize.Medium}></ak-spinner>
|
||||
</span>`
|
||||
: ""}
|
||||
<slot></slot>
|
||||
</button>`;
|
||||
|
|
|
@ -17,23 +17,25 @@ export class TokenCopyButton extends ActionButton {
|
|||
if (!this.identifier) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTokensViewKeyRetrieve({
|
||||
identifier: this.identifier
|
||||
}).then((token) => {
|
||||
if (!token.key) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return navigator.clipboard.writeText(token.key).then(() => {
|
||||
this.buttonClass = SUCCESS_CLASS;
|
||||
setTimeout(() => {
|
||||
this.buttonClass = PRIMARY_CLASS;
|
||||
}, 1500);
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTokensViewKeyRetrieve({
|
||||
identifier: this.identifier,
|
||||
})
|
||||
.then((token) => {
|
||||
if (!token.key) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return navigator.clipboard.writeText(token.key).then(() => {
|
||||
this.buttonClass = SUCCESS_CLASS;
|
||||
setTimeout(() => {
|
||||
this.buttonClass = PRIMARY_CLASS;
|
||||
}, 1500);
|
||||
});
|
||||
})
|
||||
.catch((err: Response | undefined) => {
|
||||
return err?.json().then((errResp) => {
|
||||
throw new Error(errResp["detail"]);
|
||||
});
|
||||
});
|
||||
}).catch((err: Response | undefined) => {
|
||||
return err?.json().then(errResp => {
|
||||
throw new Error(errResp["detail"]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
|
@ -17,22 +25,24 @@ export class AggregateCard extends LitElement {
|
|||
headerLink?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFFlex, AKGlobal].concat([css`
|
||||
.pf-c-card.pf-c-card-aggregate {
|
||||
height: 100%;
|
||||
}
|
||||
.pf-c-card__header {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.center-value {
|
||||
font-size: var(--pf-global--icon--FontSize--lg);
|
||||
text-align: center;
|
||||
color: var(--pf-global--Color--100);
|
||||
}
|
||||
.subtext {
|
||||
font-size: var(--pf-global--FontSize--sm);
|
||||
}
|
||||
`]);
|
||||
return [PFBase, PFCard, PFFlex, AKGlobal].concat([
|
||||
css`
|
||||
.pf-c-card.pf-c-card-aggregate {
|
||||
height: 100%;
|
||||
}
|
||||
.pf-c-card__header {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.center-value {
|
||||
font-size: var(--pf-global--icon--FontSize--lg);
|
||||
text-align: center;
|
||||
color: var(--pf-global--Color--100);
|
||||
}
|
||||
.subtext {
|
||||
font-size: var(--pf-global--FontSize--sm);
|
||||
}
|
||||
`,
|
||||
]);
|
||||
}
|
||||
|
||||
renderInner(): TemplateResult {
|
||||
|
@ -40,9 +50,11 @@ export class AggregateCard extends LitElement {
|
|||
}
|
||||
|
||||
renderHeaderLink(): TemplateResult {
|
||||
return html`${this.headerLink ? html`<a href="${this.headerLink}">
|
||||
<i class="fa fa-link"> </i>
|
||||
</a>` : ""}`;
|
||||
return html`${this.headerLink
|
||||
? html`<a href="${this.headerLink}">
|
||||
<i class="fa fa-link"> </i>
|
||||
</a>`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -53,10 +65,7 @@ export class AggregateCard extends LitElement {
|
|||
</div>
|
||||
${this.renderHeaderLink()}
|
||||
</div>
|
||||
<div class="pf-c-card__body center-value">
|
||||
${this.renderInner()}
|
||||
</div>
|
||||
<div class="pf-c-card__body center-value">${this.renderInner()}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@ import { PFSize } from "../Spinner";
|
|||
|
||||
@customElement("ak-aggregate-card-promise")
|
||||
export class AggregatePromiseCard extends AggregateCard {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
promise?: Promise<Record<string, unknown>>;
|
||||
|
||||
promiseProxy(): Promise<TemplateResult> {
|
||||
if (!this.promise) {
|
||||
return new Promise<TemplateResult>(() => html``);
|
||||
}
|
||||
return this.promise.then(s => {
|
||||
return this.promise.then((s) => {
|
||||
return html`<i class="fa fa-check-circle"></i> ${s.toString()}`;
|
||||
});
|
||||
}
|
||||
|
@ -23,5 +23,4 @@ export class AggregatePromiseCard extends AggregateCard {
|
|||
${until(this.promiseProxy(), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
|||
|
||||
@customElement("ak-charts-admin-login")
|
||||
export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
||||
|
||||
apiRequest(): Promise<LoginMetrics> {
|
||||
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
||||
}
|
||||
|
@ -18,26 +17,27 @@ export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
|||
label: "Failed Logins",
|
||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||
spanGaps: true,
|
||||
data: data.loginsFailedPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.loginsFailedPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
label: "Successful Logins",
|
||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||
spanGaps: true,
|
||||
data: data.loginsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.loginsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ import { ChartData } from "chart.js";
|
|||
|
||||
@customElement("ak-charts-application-authorize")
|
||||
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
||||
|
||||
@property()
|
||||
applicationSlug!: string;
|
||||
|
||||
apiRequest(): Promise<Coordinate[]> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({ slug: this.applicationSlug });
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({
|
||||
slug: this.applicationSlug,
|
||||
});
|
||||
}
|
||||
|
||||
getChartData(data: Coordinate[]): ChartData {
|
||||
|
@ -21,15 +22,15 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
|||
label: "Authorizations",
|
||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||
spanGaps: true,
|
||||
data: data.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ArcElement, BarElement } from "chart.js";
|
|||
import { TimeScale, LinearScale } from "chart.js";
|
||||
import "chartjs-adapter-moment";
|
||||
import { FONT_COLOUR_DARK_MODE, FONT_COLOUR_LIGHT_MODE } from "../../pages/flows/FlowDiagram";
|
||||
import {EVENT_REFRESH} from "../../constants";
|
||||
import { EVENT_REFRESH } from "../../constants";
|
||||
|
||||
Chart.register(Legend, Tooltip);
|
||||
Chart.register(LineController, BarController, DoughnutController);
|
||||
|
@ -14,7 +14,6 @@ Chart.register(ArcElement, BarElement);
|
|||
Chart.register(TimeScale, LinearScale);
|
||||
|
||||
export abstract class AKChart<T> extends LitElement {
|
||||
|
||||
abstract apiRequest(): Promise<T>;
|
||||
abstract getChartData(data: T): ChartData;
|
||||
|
||||
|
@ -26,15 +25,17 @@ export abstract class AKChart<T> extends LitElement {
|
|||
fontColour = FONT_COLOUR_LIGHT_MODE;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [css`
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
canvas {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
css`
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
canvas {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -99,12 +100,14 @@ export abstract class AKChart<T> extends LitElement {
|
|||
chart.ctx.textBaseline = "middle";
|
||||
chart.ctx.fillStyle = this.fontColour;
|
||||
|
||||
const textX = Math.round((width - chart.ctx.measureText(this.centerText).width) / 2);
|
||||
const textX = Math.round(
|
||||
(width - chart.ctx.measureText(this.centerText).width) / 2,
|
||||
);
|
||||
const textY = height / 2;
|
||||
|
||||
chart.ctx.fillText(this.centerText, textX, textY);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -116,8 +119,12 @@ export abstract class AKChart<T> extends LitElement {
|
|||
type: "time",
|
||||
display: true,
|
||||
ticks: {
|
||||
callback: function (tickValue: string | number, index: number, ticks: Tick[]): string {
|
||||
const valueStamp = (ticks[index]);
|
||||
callback: function (
|
||||
tickValue: string | number,
|
||||
index: number,
|
||||
ticks: Tick[],
|
||||
): string {
|
||||
const valueStamp = ticks[index];
|
||||
const delta = Date.now() - valueStamp.value;
|
||||
const ago = Math.round(delta / 1000 / 3600);
|
||||
return `${ago} Hours ago`;
|
||||
|
@ -129,7 +136,7 @@ export abstract class AKChart<T> extends LitElement {
|
|||
grid: {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
offset: true
|
||||
offset: true,
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
|
@ -138,7 +145,7 @@ export abstract class AKChart<T> extends LitElement {
|
|||
grid: {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
} as ChartOptions;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@ import { ChartData } from "chart.js";
|
|||
|
||||
@customElement("ak-charts-user")
|
||||
export class UserChart extends AKChart<UserMetrics> {
|
||||
|
||||
@property({type: Number})
|
||||
@property({ type: Number })
|
||||
userId?: number;
|
||||
|
||||
apiRequest(): Promise<UserMetrics> {
|
||||
|
@ -23,37 +22,39 @@ export class UserChart extends AKChart<UserMetrics> {
|
|||
label: "Failed Logins",
|
||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||
spanGaps: true,
|
||||
data: data.loginsFailedPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.loginsFailedPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
label: "Successful Logins",
|
||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||
spanGaps: true,
|
||||
data: data.loginsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.loginsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
label: "Application authorizations",
|
||||
backgroundColor: "rgba(43, 154, 243, .5)",
|
||||
spanGaps: true,
|
||||
data: data.authorizationsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
data:
|
||||
data.authorizationsPer1h?.map((cord) => {
|
||||
return {
|
||||
x: cord.xCord || 0,
|
||||
y: cord.yCord || 0,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,11 +7,10 @@ import AKGlobal from "../../authentik.css";
|
|||
|
||||
@customElement("ak-chip")
|
||||
export class Chip extends LitElement {
|
||||
|
||||
@property()
|
||||
value?: number | string;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
removable = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -24,16 +23,23 @@ export class Chip extends LitElement {
|
|||
<span class="pf-c-chip__text">
|
||||
<slot></slot>
|
||||
</span>
|
||||
${this.removable ? html`<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
|
||||
this.dispatchEvent(new CustomEvent("remove", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}}>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>` : html``}
|
||||
${this.removable
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("remove", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>`
|
||||
: html``}
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Chip } from "./Chip";
|
|||
|
||||
@customElement("ak-chip-group")
|
||||
export class ChipGroup extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFChip, PFChipGroup, PFButton, AKGlobal];
|
||||
}
|
||||
|
@ -20,7 +19,7 @@ export class ChipGroup extends LitElement {
|
|||
|
||||
get value(): (string | number | undefined)[] {
|
||||
const values: (string | number | undefined)[] = [];
|
||||
this.querySelectorAll<Chip>("ak-chip").forEach(chip => {
|
||||
this.querySelectorAll<Chip>("ak-chip").forEach((chip) => {
|
||||
values.push(chip.value);
|
||||
});
|
||||
return values;
|
||||
|
@ -28,12 +27,11 @@ export class ChipGroup extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-chip-group">
|
||||
<div class="pf-c-chip-group__main">
|
||||
<ul class="pf-c-chip-group__list" role="list">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="pf-c-chip-group__main">
|
||||
<ul class="pf-c-chip-group__list" role="list">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -55,32 +55,28 @@ export class ObjectChangelog extends Table<Event> {
|
|||
return [
|
||||
html`${item.action}`,
|
||||
html`<div>${item.user?.username}</div>
|
||||
${item.user.on_behalf_of ? html`<small>
|
||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
||||
</small>` : html``}`,
|
||||
${item.user.on_behalf_of
|
||||
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
|
||||
: html``}`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp || "-"}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: Event): TemplateResult {
|
||||
return html`
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
return html` <td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||
<div slot="body">
|
||||
${t`No matching events could be found.`}
|
||||
</div>
|
||||
<div slot="body">${t`No matching events could be found.`}</div>
|
||||
</ak-empty-state>`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class ObjectChangelog extends Table<Event> {
|
|||
page: page,
|
||||
ordering: this.order,
|
||||
pageSize: PAGE_SIZE / 2,
|
||||
username: this.targetUser
|
||||
username: this.targetUser,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,32 +46,28 @@ export class ObjectChangelog extends Table<Event> {
|
|||
return [
|
||||
html`${item.action}`,
|
||||
html`<div>${item.user?.username}</div>
|
||||
${item.user.on_behalf_of ? html`<small>
|
||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
||||
</small>` : html``}`,
|
||||
${item.user.on_behalf_of
|
||||
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
|
||||
: html``}`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp || "-"}</span>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: Event): TemplateResult {
|
||||
return html`
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
return html` <td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
}
|
||||
|
||||
renderEmpty(): TemplateResult {
|
||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||
<div slot="body">
|
||||
${t`No matching events could be found.`}
|
||||
</div>
|
||||
<div slot="body">${t`No matching events could be found.`}</div>
|
||||
</ak-empty-state>`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import { showMessage } from "../messages/MessageContainer";
|
|||
|
||||
@customElement("ak-forms-confirm")
|
||||
export class ConfirmationForm extends ModalButton {
|
||||
|
||||
@property()
|
||||
successMessage!: string;
|
||||
@property()
|
||||
|
@ -17,23 +16,25 @@ export class ConfirmationForm extends ModalButton {
|
|||
@property()
|
||||
action!: string;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
onConfirm!: () => Promise<unknown>;
|
||||
|
||||
confirm(): Promise<void> {
|
||||
return this.onConfirm().then(() => {
|
||||
this.onSuccess();
|
||||
this.open = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}).catch((e) => {
|
||||
this.onError(e);
|
||||
throw e;
|
||||
});
|
||||
return this.onConfirm()
|
||||
.then(() => {
|
||||
this.onSuccess();
|
||||
this.open = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.onError(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess(): void {
|
||||
|
@ -52,33 +53,34 @@ export class ConfirmationForm extends ModalButton {
|
|||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
<slot name="header"></slot>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<slot class="pf-c-content" name="body"></slot>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-danger">
|
||||
${this.action}
|
||||
</ak-spinner-button>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary">
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
<slot name="header"></slot>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<slot class="pf-c-content" name="body"></slot>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-danger"
|
||||
>
|
||||
${this.action} </ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,42 +11,43 @@ import { until } from "lit-html/directives/until";
|
|||
|
||||
@customElement("ak-forms-delete")
|
||||
export class DeleteForm extends ModalButton {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFList);
|
||||
}
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
obj?: Record<string, unknown>;
|
||||
|
||||
@property()
|
||||
objectLabel?: string;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
usedBy?: () => Promise<UsedBy[]>;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
delete!: () => Promise<unknown>;
|
||||
|
||||
confirm(): Promise<void> {
|
||||
return this.delete().then(() => {
|
||||
this.onSuccess();
|
||||
this.open = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}).catch((e) => {
|
||||
this.onError(e);
|
||||
throw e;
|
||||
});
|
||||
return this.delete()
|
||||
.then(() => {
|
||||
this.onSuccess();
|
||||
this.open = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.onError(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess(): void {
|
||||
showMessage({
|
||||
message: t`Successfully deleted ${this.objectLabel} ${ this.obj?.name }`,
|
||||
message: t`Successfully deleted ${this.objectLabel} ${this.obj?.name}`,
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
}
|
||||
|
@ -66,69 +67,70 @@ export class DeleteForm extends ModalButton {
|
|||
objName = "";
|
||||
}
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
${t`Delete ${this.objectLabel}`}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<p>
|
||||
${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
${this.usedBy ? until(this.usedBy().then(usedBy => {
|
||||
if (usedBy.length < 1) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<section class="pf-c-page__main-section">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<p>
|
||||
${t`The following objects use ${objName} `}
|
||||
</p>
|
||||
<ul class="pf-c-list">
|
||||
${usedBy.map(ub => {
|
||||
let consequence = "";
|
||||
switch (ub.action) {
|
||||
case UsedByActionEnum.Cascade:
|
||||
consequence = t`object will be DELETED`;
|
||||
break;
|
||||
case UsedByActionEnum.CascadeMany:
|
||||
consequence = t`connecting object will be deleted`;
|
||||
break;
|
||||
case UsedByActionEnum.SetDefault:
|
||||
consequence = t`reference will be reset to default value`;
|
||||
break;
|
||||
case UsedByActionEnum.SetNull:
|
||||
consequence = t`reference will be set to an empty value`;
|
||||
break;
|
||||
}
|
||||
return html`<li>${t`${ub.name} (${consequence})`}</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
})) : html``}
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-danger">
|
||||
${t`Delete`}
|
||||
</ak-spinner-button>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary">
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">${t`Delete ${this.objectLabel}`}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<p>${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}</p>
|
||||
</form>
|
||||
</section>
|
||||
${this.usedBy
|
||||
? until(
|
||||
this.usedBy().then((usedBy) => {
|
||||
if (usedBy.length < 1) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<section class="pf-c-page__main-section">
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<p>${t`The following objects use ${objName} `}</p>
|
||||
<ul class="pf-c-list">
|
||||
${usedBy.map((ub) => {
|
||||
let consequence = "";
|
||||
switch (ub.action) {
|
||||
case UsedByActionEnum.Cascade:
|
||||
consequence = t`object will be DELETED`;
|
||||
break;
|
||||
case UsedByActionEnum.CascadeMany:
|
||||
consequence = t`connecting object will be deleted`;
|
||||
break;
|
||||
case UsedByActionEnum.SetDefault:
|
||||
consequence = t`reference will be reset to default value`;
|
||||
break;
|
||||
case UsedByActionEnum.SetNull:
|
||||
consequence = t`reference will be set to an empty value`;
|
||||
break;
|
||||
}
|
||||
return html`<li>
|
||||
${t`${ub.name} (${consequence})`}
|
||||
</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
}),
|
||||
)
|
||||
: html``}
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-danger"
|
||||
>
|
||||
${t`Delete`} </ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,15 @@ import "@polymer/paper-input/paper-input";
|
|||
import "@polymer/iron-form/iron-form";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
@ -18,31 +26,38 @@ import { ValidationError } from "authentik-api";
|
|||
import { EVENT_REFRESH } from "../../constants";
|
||||
|
||||
export class APIError extends Error {
|
||||
|
||||
constructor(public response: ValidationError) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@customElement("ak-form")
|
||||
export class Form<T> extends LitElement {
|
||||
|
||||
@property()
|
||||
successMessage = "";
|
||||
|
||||
@property()
|
||||
send!: (data: T) => Promise<unknown>;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
nonFieldErrors?: string[];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFInputGroup, PFFormControl, AKGlobal, css`
|
||||
select[multiple] {
|
||||
height: 15em;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFCard,
|
||||
PFButton,
|
||||
PFForm,
|
||||
PFAlert,
|
||||
PFInputGroup,
|
||||
PFFormControl,
|
||||
AKGlobal,
|
||||
css`
|
||||
select[multiple] {
|
||||
height: 15em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
get isInViewport(): boolean {
|
||||
|
@ -55,25 +70,27 @@ export class Form<T> extends LitElement {
|
|||
}
|
||||
|
||||
updated(): void {
|
||||
this.shadowRoot?.querySelectorAll<HTMLInputElement>("input[name=name]").forEach(nameInput => {
|
||||
const form = nameInput.closest("form");
|
||||
if (form === null) {
|
||||
return;
|
||||
}
|
||||
const slugField = form.querySelector<HTMLInputElement>("input[name=slug]");
|
||||
if (!slugField) {
|
||||
return;
|
||||
}
|
||||
// Only attach handler if the slug is already equal to the name
|
||||
// if not, they are probably completely different and shouldn't update
|
||||
// each other
|
||||
if (convertToSlug(nameInput.value) !== slugField.value) {
|
||||
return;
|
||||
}
|
||||
nameInput.addEventListener("input", () => {
|
||||
slugField.value = convertToSlug(nameInput.value);
|
||||
this.shadowRoot
|
||||
?.querySelectorAll<HTMLInputElement>("input[name=name]")
|
||||
.forEach((nameInput) => {
|
||||
const form = nameInput.closest("form");
|
||||
if (form === null) {
|
||||
return;
|
||||
}
|
||||
const slugField = form.querySelector<HTMLInputElement>("input[name=slug]");
|
||||
if (!slugField) {
|
||||
return;
|
||||
}
|
||||
// Only attach handler if the slug is already equal to the name
|
||||
// if not, they are probably completely different and shouldn't update
|
||||
// each other
|
||||
if (convertToSlug(nameInput.value) !== slugField.value) {
|
||||
return;
|
||||
}
|
||||
nameInput.addEventListener("input", () => {
|
||||
slugField.value = convertToSlug(nameInput.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,7 +127,7 @@ export class Form<T> extends LitElement {
|
|||
serializeForm(form: IronFormElement): T {
|
||||
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
||||
const json: { [key: string]: unknown } = {};
|
||||
elements.forEach(element => {
|
||||
elements.forEach((element) => {
|
||||
const values = form._serializeElementValues(element);
|
||||
if (element.hidden) {
|
||||
return;
|
||||
|
@ -138,54 +155,58 @@ export class Form<T> extends LitElement {
|
|||
return;
|
||||
}
|
||||
const data = this.serializeForm(ironForm);
|
||||
return this.send(data).then((r) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: this.getSuccessMessage()
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
return r;
|
||||
}).catch((ex: Response | Error) => {
|
||||
if (ex instanceof Error) {
|
||||
throw ex;
|
||||
}
|
||||
if (ex.status > 399 && ex.status < 500) {
|
||||
return ex.json().then((errorMessage: ValidationError) => {
|
||||
if (!errorMessage) return errorMessage;
|
||||
if (errorMessage instanceof Error) {
|
||||
throw errorMessage;
|
||||
}
|
||||
// assign all input-related errors to their elements
|
||||
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
|
||||
elements.forEach((element) => {
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
if (camelToSnake(elementName) in errorMessage) {
|
||||
element.errorMessage = errorMessage[camelToSnake(elementName)].join(", ");
|
||||
element.invalid = true;
|
||||
}
|
||||
});
|
||||
if ("non_field_errors" in errorMessage) {
|
||||
this.nonFieldErrors = errorMessage["non_field_errors"];
|
||||
}
|
||||
throw new APIError(errorMessage);
|
||||
return this.send(data)
|
||||
.then((r) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: this.getSuccessMessage(),
|
||||
});
|
||||
}
|
||||
throw ex;
|
||||
}).catch((ex: Error) => {
|
||||
// error is local or not from rest_framework
|
||||
showMessage({
|
||||
message: ex.toString(),
|
||||
level: MessageLevel.error,
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
return r;
|
||||
})
|
||||
.catch((ex: Response | Error) => {
|
||||
if (ex instanceof Error) {
|
||||
throw ex;
|
||||
}
|
||||
if (ex.status > 399 && ex.status < 500) {
|
||||
return ex.json().then((errorMessage: ValidationError) => {
|
||||
if (!errorMessage) return errorMessage;
|
||||
if (errorMessage instanceof Error) {
|
||||
throw errorMessage;
|
||||
}
|
||||
// assign all input-related errors to their elements
|
||||
const elements: PaperInputElement[] = ironForm._getSubmittableElements();
|
||||
elements.forEach((element) => {
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
if (camelToSnake(elementName) in errorMessage) {
|
||||
element.errorMessage =
|
||||
errorMessage[camelToSnake(elementName)].join(", ");
|
||||
element.invalid = true;
|
||||
}
|
||||
});
|
||||
if ("non_field_errors" in errorMessage) {
|
||||
this.nonFieldErrors = errorMessage["non_field_errors"];
|
||||
}
|
||||
throw new APIError(errorMessage);
|
||||
});
|
||||
}
|
||||
throw ex;
|
||||
})
|
||||
.catch((ex: Error) => {
|
||||
// error is local or not from rest_framework
|
||||
showMessage({
|
||||
message: ex.toString(),
|
||||
level: MessageLevel.error,
|
||||
});
|
||||
// rethrow the error so the form doesn't close
|
||||
throw ex;
|
||||
});
|
||||
// rethrow the error so the form doesn't close
|
||||
throw ex;
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
|
@ -197,24 +218,24 @@ export class Form<T> extends LitElement {
|
|||
return html``;
|
||||
}
|
||||
return html`<div class="pf-c-form__alert">
|
||||
${this.nonFieldErrors.map(err => {
|
||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">
|
||||
${err}
|
||||
</h4>
|
||||
</div>`;
|
||||
})}
|
||||
${this.nonFieldErrors.map((err) => {
|
||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">${err}</h4>
|
||||
</div>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderVisible(): TemplateResult {
|
||||
return html`<iron-form
|
||||
@iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}>
|
||||
${this.renderNonFieldErrors()}
|
||||
${this.renderForm()}
|
||||
@iron-form-presubmit=${(ev: Event) => {
|
||||
this.submit(ev);
|
||||
}}
|
||||
>
|
||||
${this.renderNonFieldErrors()} ${this.renderForm()}
|
||||
</iron-form>`;
|
||||
}
|
||||
|
||||
|
@ -224,5 +245,4 @@ export class Form<T> extends LitElement {
|
|||
}
|
||||
return this.renderVisible();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,16 +6,19 @@ import { ErrorDetail } from "authentik-api";
|
|||
|
||||
@customElement("ak-form-element")
|
||||
export class FormElement extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFForm, PFFormControl, css`
|
||||
slot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
css`
|
||||
slot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@property()
|
||||
|
@ -28,22 +31,23 @@ export class FormElement extends LitElement {
|
|||
errors?: ErrorDetail[];
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text">${this.label}</span>
|
||||
${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``}
|
||||
</label>
|
||||
<slot></slot>
|
||||
${(this.errors || []).map((error) => {
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`;
|
||||
})}
|
||||
</div>`;
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text">${this.label}</span>
|
||||
${this.required
|
||||
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
|
||||
: html``}
|
||||
</label>
|
||||
<slot></slot>
|
||||
${(this.errors || []).map((error) => {
|
||||
return html`<p class="pf-c-form__helper-text pf-m-error">${error.string}</p>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
|
@ -7,25 +15,37 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|||
|
||||
@customElement("ak-form-group")
|
||||
export class FormGroup extends LitElement {
|
||||
|
||||
@property({ type: Boolean })
|
||||
expanded = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFForm, PFButton, PFFormControl, AKGlobal, css`
|
||||
slot[name=body][hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFForm,
|
||||
PFButton,
|
||||
PFFormControl,
|
||||
AKGlobal,
|
||||
css`
|
||||
slot[name="body"][hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
||||
<div class="pf-c-form__field-group-toggle">
|
||||
<div class="pf-c-form__field-group-toggle-button">
|
||||
<button class="pf-c-button pf-m-plain" type="button" aria-expanded="${this.expanded}" aria-label="Details" @click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
aria-expanded="${this.expanded}"
|
||||
aria-label="Details"
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-form__field-group-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
@ -47,5 +67,4 @@ export class FormGroup extends LitElement {
|
|||
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,17 +8,24 @@ import { t } from "@lingui/macro";
|
|||
|
||||
@customElement("ak-form-element-horizontal")
|
||||
export class HorizontalFormElement extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFForm, PFFormControl, AKGlobal, css`
|
||||
.pf-c-form__group {
|
||||
display: grid;
|
||||
grid-template-columns: var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
|
||||
}
|
||||
.pf-c-form__group-label {
|
||||
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
AKGlobal,
|
||||
css`
|
||||
.pf-c-form__group {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
|
||||
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
|
||||
}
|
||||
.pf-c-form__group-label {
|
||||
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@property()
|
||||
|
@ -43,7 +50,7 @@ export class HorizontalFormElement extends LitElement {
|
|||
name = "";
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
});
|
||||
this.querySelectorAll("*").forEach((input) => {
|
||||
|
@ -59,7 +66,7 @@ export class HorizontalFormElement extends LitElement {
|
|||
return;
|
||||
}
|
||||
if (this.writeOnly && !this.writeOnlyActivated) {
|
||||
const i = (input as HTMLInputElement);
|
||||
const i = input as HTMLInputElement;
|
||||
i.setAttribute("hidden", "true");
|
||||
const handler = () => {
|
||||
i.removeAttribute("hidden");
|
||||
|
@ -76,24 +83,36 @@ export class HorizontalFormElement extends LitElement {
|
|||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label">
|
||||
<span class="pf-c-form__label-text">${this.label}</span>
|
||||
${this.required ? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>` : html``}
|
||||
${this.required
|
||||
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
|
||||
: html``}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
${this.writeOnly && !this.writeOnlyActivated ?
|
||||
html`<div class="pf-c-form__horizontal-group">
|
||||
<input class="pf-c-form-control" type="password" disabled value="**************">
|
||||
</div>` :
|
||||
html``}
|
||||
${this.writeOnly && !this.writeOnlyActivated
|
||||
? html`<div class="pf-c-form__horizontal-group">
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
type="password"
|
||||
disabled
|
||||
value="**************"
|
||||
/>
|
||||
</div>`
|
||||
: html``}
|
||||
<slot class="pf-c-form__horizontal-group"></slot>
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
${this.writeOnly ? html`<p class="pf-c-form__helper-text" aria-live="polite">${
|
||||
t`Click to change value`
|
||||
}</p>` : html``}
|
||||
${this.invalid ? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">${this.errorMessage}</p>` : html``}
|
||||
${this.writeOnly
|
||||
? html`<p class="pf-c-form__helper-text" aria-live="polite">
|
||||
${t`Click to change value`}
|
||||
</p>`
|
||||
: html``}
|
||||
${this.invalid
|
||||
? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
|
||||
${this.errorMessage}
|
||||
</p>`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,11 +7,10 @@ import "../buttons/SpinnerButton";
|
|||
|
||||
@customElement("ak-forms-modal")
|
||||
export class ModalForm extends ModalButton {
|
||||
|
||||
@property({ type: Boolean })
|
||||
closeAfterSuccessfulSubmit = true;
|
||||
|
||||
confirm(): Promise<void> {
|
||||
confirm(): Promise<void> {
|
||||
const form = this.querySelector<Form<unknown>>("[slot=form]");
|
||||
if (!form) {
|
||||
return Promise.reject(t`No form found`);
|
||||
|
@ -29,39 +28,40 @@ export class ModalForm extends ModalButton {
|
|||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
<slot name="header"></slot>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<slot name="form"></slot>
|
||||
</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-primary">
|
||||
<slot name="submit"></slot>
|
||||
</ak-spinner-button>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.resetForms();
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary">
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">
|
||||
<slot name="header"></slot>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<slot name="form"></slot>
|
||||
</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${() => {
|
||||
return this.confirm();
|
||||
}}
|
||||
class="pf-m-primary"
|
||||
>
|
||||
<slot name="submit"></slot> </ak-spinner-button
|
||||
>
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.resetForms();
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${t`Cancel`}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ import { EVENT_REFRESH } from "../../constants";
|
|||
import { Form } from "./Form";
|
||||
|
||||
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
||||
|
||||
abstract loadInstance(pk: PKT): Promise<T>;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
set instancePk(value: PKT) {
|
||||
this._instancePk = value;
|
||||
if (this.isInViewport) {
|
||||
this.loadInstance(value).then(instance => {
|
||||
this.loadInstance(value).then((instance) => {
|
||||
this.instance = instance;
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
@ -32,7 +31,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
|||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
if (!this._instancePk) return;
|
||||
this.loadInstance(this._instancePk).then(instance => {
|
||||
this.loadInstance(this._instancePk).then((instance) => {
|
||||
this.instance = instance;
|
||||
});
|
||||
});
|
||||
|
@ -51,5 +50,4 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
|||
}
|
||||
return super.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ import { Form } from "./Form";
|
|||
|
||||
@customElement("ak-proxy-form")
|
||||
export class ProxyForm extends Form<unknown> {
|
||||
|
||||
@property()
|
||||
type!: string;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
args: Record<string, unknown> = {};
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
typeMap: Record<string, string> = {};
|
||||
|
||||
submit(ev: Event): Promise<unknown> | undefined {
|
||||
|
@ -43,5 +42,4 @@ export class ProxyForm extends Form<unknown> {
|
|||
}
|
||||
return html`${el}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export enum MessageLevel {
|
|||
error = "error",
|
||||
warning = "warning",
|
||||
success = "success",
|
||||
info = "info"
|
||||
info = "info",
|
||||
}
|
||||
export interface APIMessage {
|
||||
level: MessageLevel;
|
||||
|
@ -27,14 +27,13 @@ const LEVEL_ICON_MAP: { [key: string]: string } = {
|
|||
|
||||
@customElement("ak-message")
|
||||
export class Message extends LitElement {
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
message?: APIMessage;
|
||||
|
||||
@property({type: Number})
|
||||
@property({ type: Number })
|
||||
removeAfter = 8000;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
onRemove?: (m: APIMessage) => void;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -51,27 +50,34 @@ export class Message extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html`<li class="pf-c-alert-group__item">
|
||||
<div class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level === MessageLevel.error ? "pf-m-danger" : ""}">
|
||||
<div
|
||||
class="pf-c-alert pf-m-${this.message?.level} ${this.message?.level ===
|
||||
MessageLevel.error
|
||||
? "pf-m-danger"
|
||||
: ""}"
|
||||
>
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="${this.message ? LEVEL_ICON_MAP[this.message.level] : ""}"></i>
|
||||
</div>
|
||||
<p class="pf-c-alert__title">
|
||||
${this.message?.message}
|
||||
</p>
|
||||
${this.message?.description && html`<div class="pf-c-alert__description">
|
||||
<p class="pf-c-alert__title">${this.message?.message}</p>
|
||||
${this.message?.description &&
|
||||
html`<div class="pf-c-alert__description">
|
||||
<p>${this.message.description}</p>
|
||||
</div>`}
|
||||
<div class="pf-c-alert__action">
|
||||
<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
|
||||
if (!this.message) return;
|
||||
if (!this.onRemove) return;
|
||||
this.onRemove(this.message);
|
||||
}}>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
if (!this.message) return;
|
||||
if (!this.onRemove) return;
|
||||
this.onRemove(this.message);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { LitElement, html, customElement, TemplateResult, property, CSSResult, css } from "lit-element";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
property,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "./Message";
|
||||
import { APIMessage } from "./Message";
|
||||
import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css";
|
||||
|
@ -17,17 +25,20 @@ export function showMessage(message: APIMessage): void {
|
|||
|
||||
@customElement("ak-message-container")
|
||||
export class MessageContainer extends LitElement {
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
messages: APIMessage[] = [];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFAlertGroup, css`
|
||||
/* Fix spacing between messages */
|
||||
ak-message {
|
||||
display: block;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFAlertGroup,
|
||||
css`
|
||||
/* Fix spacing between messages */
|
||||
ak-message {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -40,7 +51,7 @@ export class MessageContainer extends LitElement {
|
|||
|
||||
// add a new message, but only if the message isn't currently shown.
|
||||
addMessage(message: APIMessage): void {
|
||||
const matchingMessages = this.messages.filter(m => m.message == message.message);
|
||||
const matchingMessages = this.messages.filter((m) => m.message == message.message);
|
||||
if (matchingMessages.length < 1) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
@ -54,9 +65,10 @@ export class MessageContainer extends LitElement {
|
|||
.onRemove=${(m: APIMessage) => {
|
||||
this.messages = this.messages.filter((v) => v !== m);
|
||||
this.requestUpdate();
|
||||
}}>
|
||||
</ak-message>`;
|
||||
})}
|
||||
}}
|
||||
>
|
||||
</ak-message>`;
|
||||
})}
|
||||
</ul>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,12 @@ import { MessageLevel } from "./Message";
|
|||
import { showMessage } from "./MessageContainer";
|
||||
|
||||
export class MessageMiddleware implements Middleware {
|
||||
|
||||
post(context: ResponseContext): Promise<Response | void> {
|
||||
if (context.response.status >= 500) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`API request failed`,
|
||||
description: `${context.init.method} ${context.url}: ${context.response.status}`
|
||||
description: `${context.init.method} ${context.url}: ${context.response.status}`,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(context.response);
|
||||
|
|
|
@ -32,7 +32,7 @@ export class APIMiddleware implements Middleware {
|
|||
new CustomEvent(EVENT_API_DRAWER_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
return Promise.resolve(context.response);
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ export const API_DRAWER_MIDDLEWARE = new APIMiddleware();
|
|||
|
||||
@customElement("ak-api-drawer")
|
||||
export class APIDrawer extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal];
|
||||
}
|
||||
|
@ -58,9 +57,7 @@ export class APIDrawer extends LitElement {
|
|||
renderItem(item: RequestInfo): TemplateResult {
|
||||
return html`<li class="pf-c-notification-drawer__list-item pf-m-read">
|
||||
<div class="pf-c-notification-drawer__list-item-header">
|
||||
<h2 class="pf-c-notification-drawer__list-item-header-title">
|
||||
${item.method}
|
||||
</h2>
|
||||
<h2 class="pf-c-notification-drawer__list-item-header-title">${item.method}</h2>
|
||||
</div>
|
||||
<p class="pf-c-notification-drawer__list-item-description">${item.path}</p>
|
||||
</li>`;
|
||||
|
@ -70,17 +67,14 @@ export class APIDrawer extends LitElement {
|
|||
return html`<div class="pf-c-drawer__body pf-m-no-padding">
|
||||
<div class="pf-c-notification-drawer">
|
||||
<div class="pf-c-notification-drawer__header pf-c-content">
|
||||
<h1>
|
||||
${t`API Requests`}
|
||||
</h1>
|
||||
<h1>${t`API Requests`}</h1>
|
||||
</div>
|
||||
<div class="pf-c-notification-drawer__body">
|
||||
<ul class="pf-c-notification-drawer__list">
|
||||
${API_DRAWER_MIDDLEWARE.requests.map(n => this.renderItem(n))}
|
||||
${API_DRAWER_MIDDLEWARE.requests.map((n) => this.renderItem(n))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { t } from "@lingui/macro";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { EventsApi, Notification } from "authentik-api";
|
||||
import { AKResponse } from "../../api/Client";
|
||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||
|
@ -14,11 +22,10 @@ import { ActionToLabel } from "../../pages/events/utils";
|
|||
|
||||
@customElement("ak-notification-drawer")
|
||||
export class NotificationDrawer extends LitElement {
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
notifications?: AKResponse<Notification>;
|
||||
|
||||
@property({type: Number})
|
||||
@property({ type: Number })
|
||||
unread = 0;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -36,34 +43,36 @@ export class NotificationDrawer extends LitElement {
|
|||
.pf-c-notification-drawer__list-item-description {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
}).then(r => {
|
||||
this.notifications = r;
|
||||
this.unread = r.results.length;
|
||||
});
|
||||
new EventsApi(DEFAULT_CONFIG)
|
||||
.eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
})
|
||||
.then((r) => {
|
||||
this.notifications = r;
|
||||
this.unread = r.results.length;
|
||||
});
|
||||
}
|
||||
|
||||
renderItem(item: Notification): TemplateResult {
|
||||
let level = "";
|
||||
switch (item.severity) {
|
||||
case "notice":
|
||||
level = "pf-m-info";
|
||||
break;
|
||||
case "warning":
|
||||
level = "pf-m-warning";
|
||||
break;
|
||||
case "alert":
|
||||
level = "pf-m-danger";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case "notice":
|
||||
level = "pf-m-info";
|
||||
break;
|
||||
case "warning":
|
||||
level = "pf-m-warning";
|
||||
break;
|
||||
case "alert":
|
||||
level = "pf-m-danger";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return html`<li class="pf-c-notification-drawer__list-item">
|
||||
<div class="pf-c-notification-drawer__list-item-header">
|
||||
|
@ -75,26 +84,38 @@ export class NotificationDrawer extends LitElement {
|
|||
</h2>
|
||||
</div>
|
||||
<div class="pf-c-notification-drawer__list-item-action">
|
||||
${item.event && html`
|
||||
<a class="pf-c-dropdown__toggle pf-m-plain" href="#/events/log/${item.event?.pk}">
|
||||
${item.event &&
|
||||
html`
|
||||
<a
|
||||
class="pf-c-dropdown__toggle pf-m-plain"
|
||||
href="#/events/log/${item.event?.pk}"
|
||||
>
|
||||
<i class="fas fas fa-share-square"></i>
|
||||
</a>
|
||||
`}
|
||||
<button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => {
|
||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsPartialUpdate({
|
||||
uuid: item.pk || "",
|
||||
patchedNotificationRequest: {
|
||||
seen: true,
|
||||
}
|
||||
}).then(() => {
|
||||
this.firstUpdated();
|
||||
});
|
||||
}}>
|
||||
<button
|
||||
class="pf-c-dropdown__toggle pf-m-plain"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
new EventsApi(DEFAULT_CONFIG)
|
||||
.eventsNotificationsPartialUpdate({
|
||||
uuid: item.pk || "",
|
||||
patchedNotificationRequest: {
|
||||
seen: true,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.firstUpdated();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
|
||||
<small class="pf-c-notification-drawer__list-item-timestamp">${item.created?.toLocaleString()}</small>
|
||||
<small class="pf-c-notification-drawer__list-item-timestamp"
|
||||
>${item.created?.toLocaleString()}</small
|
||||
>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
|
@ -106,12 +127,8 @@ export class NotificationDrawer extends LitElement {
|
|||
<div class="pf-c-notification-drawer">
|
||||
<div class="pf-c-notification-drawer__header">
|
||||
<div class="text">
|
||||
<h1 class="pf-c-notification-drawer__header-title">
|
||||
${t`Notifications`}
|
||||
</h1>
|
||||
<span>
|
||||
${t`${this.unread} unread`}
|
||||
</span>
|
||||
<h1 class="pf-c-notification-drawer__header-title">${t`Notifications`}</h1>
|
||||
<span> ${t`${this.unread} unread`} </span>
|
||||
</div>
|
||||
<div class="pf-c-notification-drawer__header-action">
|
||||
<div class="pf-c-notification-drawer__header-action-close">
|
||||
|
@ -121,12 +138,13 @@ export class NotificationDrawer extends LitElement {
|
|||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
aria-label="Close">
|
||||
aria-label="Close"
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -134,11 +152,10 @@ export class NotificationDrawer extends LitElement {
|
|||
</div>
|
||||
<div class="pf-c-notification-drawer__body">
|
||||
<ul class="pf-c-notification-drawer__list">
|
||||
${this.notifications.results.map(n => this.renderItem(n))}
|
||||
${this.notifications.results.map((n) => this.renderItem(n))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,30 +35,27 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> {
|
|||
|
||||
row(item: ExpiringBaseGrantModel): TemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||
${item.provider?.name}
|
||||
</a>`,
|
||||
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.scope.join(", ")}`,
|
||||
html`
|
||||
<ak-forms-delete
|
||||
html` <ak-forms-delete
|
||||
.obj=${item}
|
||||
objectLabel=${t`Authorization Code`}
|
||||
.usedBy=${() => {
|
||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({
|
||||
id: item.pk
|
||||
id: item.pk,
|
||||
});
|
||||
}}
|
||||
.delete=${() => {
|
||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({
|
||||
id: item.pk,
|
||||
});
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete Authorization Code`}
|
||||
</button>
|
||||
</ak-forms-delete>`,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -42,49 +42,45 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
|||
}
|
||||
|
||||
renderExpanded(item: RefreshTokenModel): TemplateResult {
|
||||
return html`
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`ID Token`}</h3>
|
||||
<pre>${item.idToken}</pre>
|
||||
return html` <td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-l-flex">
|
||||
<div class="pf-l-flex__item">
|
||||
<h3>${t`ID Token`}</h3>
|
||||
<pre>${item.idToken}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
}
|
||||
|
||||
row(item: RefreshTokenModel): TemplateResult[] {
|
||||
return [
|
||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
||||
${item.provider?.name}
|
||||
</a>`,
|
||||
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||
html`${item.revoked ? t`Yes` : t`No`}`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.scope.join(", ")}`,
|
||||
html`
|
||||
<ak-forms-delete
|
||||
html` <ak-forms-delete
|
||||
.obj=${item}
|
||||
objectLabel=${t`Refresh Code`}
|
||||
.usedBy=${() => {
|
||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
|
||||
id: item.pk
|
||||
id: item.pk,
|
||||
});
|
||||
}}
|
||||
.delete=${() => {
|
||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
|
||||
id: item.pk,
|
||||
});
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete Refresh Code`}
|
||||
</button>
|
||||
</ak-forms-delete>`,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Route } from "./Route";
|
|||
|
||||
export class RouteMatch {
|
||||
route: Route;
|
||||
arguments: { [key: string]: string; };
|
||||
arguments: { [key: string]: string };
|
||||
fullUrl?: string;
|
||||
|
||||
constructor(route: Route) {
|
||||
|
@ -16,6 +16,8 @@ export class RouteMatch {
|
|||
}
|
||||
|
||||
toString(): string {
|
||||
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(this.arguments)}>`;
|
||||
return `<RouteMatch url=${this.fullUrl} route=${this.route} arguments=${JSON.stringify(
|
||||
this.arguments,
|
||||
)}>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||
|
||||
@customElement("ak-router-404")
|
||||
export class Router404 extends LitElement {
|
||||
|
||||
@property()
|
||||
url = "";
|
||||
|
||||
|
@ -19,9 +18,7 @@ export class Router404 extends LitElement {
|
|||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">${t`Not found`}</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
${t`The URL "${this.url}" was not found.`}
|
||||
</div>
|
||||
<div class="pf-c-empty-state__body">${t`The URL "${this.url}" was not found.`}</div>
|
||||
<a href="#/" class="pf-c-button pf-m-primary" type="button">${t`Return home`}</a>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { Route } from "./Route";
|
||||
import { ROUTES } from "../../routes";
|
||||
import { RouteMatch } from "./RouteMatch";
|
||||
|
@ -10,26 +18,36 @@ import { ROUTE_SEPARATOR } from "../../constants";
|
|||
// Poliyfill for hashchange.newURL,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
|
||||
window.addEventListener("load", () => {
|
||||
if (!window.HashChangeEvent) (function () {
|
||||
let lastURL = document.URL;
|
||||
window.addEventListener("hashchange", function (event) {
|
||||
Object.defineProperty(event, "oldURL", { enumerable: true, configurable: true, value: lastURL });
|
||||
Object.defineProperty(event, "newURL", { enumerable: true, configurable: true, value: document.URL });
|
||||
lastURL = document.URL;
|
||||
});
|
||||
}());
|
||||
if (!window.HashChangeEvent)
|
||||
(function () {
|
||||
let lastURL = document.URL;
|
||||
window.addEventListener("hashchange", function (event) {
|
||||
Object.defineProperty(event, "oldURL", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: lastURL,
|
||||
});
|
||||
Object.defineProperty(event, "newURL", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: document.URL,
|
||||
});
|
||||
lastURL = document.URL;
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
||||
@customElement("ak-router-outlet")
|
||||
export class RouterOutlet extends LitElement {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
current?: RouteMatch;
|
||||
|
||||
@property()
|
||||
defaultUrl?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [AKGlobal,
|
||||
return [
|
||||
AKGlobal,
|
||||
css`
|
||||
:host {
|
||||
height: 100vh;
|
||||
|
@ -88,7 +106,7 @@ export class RouterOutlet extends LitElement {
|
|||
RegExp(""),
|
||||
html`<div class="pf-c-page__main">
|
||||
<ak-router-404 url=${activeUrl}></ak-router-404>
|
||||
</div>`
|
||||
</div>`,
|
||||
);
|
||||
matchedRoute = new RouteMatch(route);
|
||||
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
|
||||
|
|
|
@ -9,7 +9,6 @@ import "./SidebarUser";
|
|||
|
||||
@customElement("ak-sidebar")
|
||||
export class Sidebar extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
|
@ -25,7 +33,7 @@ export const DefaultTenant: CurrentTenant = {
|
|||
|
||||
@customElement("ak-sidebar-brand")
|
||||
export class SidebarBrand extends LitElement {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
tenant: CurrentTenant = DefaultTenant;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -45,7 +53,7 @@ export class SidebarBrand extends LitElement {
|
|||
}
|
||||
.pf-c-brand img {
|
||||
width: 100%;
|
||||
padding: 0 .5rem;
|
||||
padding: 0 0.5rem;
|
||||
height: 42px;
|
||||
}
|
||||
button.pf-c-button.sidebar-trigger {
|
||||
|
@ -67,28 +75,34 @@ export class SidebarBrand extends LitElement {
|
|||
|
||||
firstUpdated(): void {
|
||||
configureSentry(true);
|
||||
tenant().then(tenant => this.tenant = tenant);
|
||||
tenant().then((tenant) => (this.tenant = tenant));
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
${window.innerWidth <= MIN_WIDTH ? html`
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
` : html``}
|
||||
return html` ${window.innerWidth <= MIN_WIDTH
|
||||
? html`
|
||||
<button
|
||||
class="sidebar-trigger pf-c-button"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
`
|
||||
: html``}
|
||||
<a href="#/" class="pf-c-page__header-brand-link">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="${ifDefined(this.tenant.brandingLogo)}" alt="authentik icon" loading="lazy" />
|
||||
<img
|
||||
src="${ifDefined(this.tenant.brandingLogo)}"
|
||||
alt="authentik icon"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { ROUTE_SEPARATOR } from "../../constants";
|
|||
|
||||
@customElement("ak-sidebar-item")
|
||||
export class SidebarItem extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
|
@ -25,7 +24,7 @@ export class SidebarItem extends LitElement {
|
|||
background-color: var(--ak-accent);
|
||||
margin: 16px;
|
||||
}
|
||||
:host([highlight]) .pf-c-nav__item .pf-c-nav__link {
|
||||
:host([highlight]) .pf-c-nav__item .pf-c-nav__link {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.pf-c-nav__link.pf-m-current::after,
|
||||
|
@ -92,13 +91,13 @@ export class SidebarItem extends LitElement {
|
|||
|
||||
get childItems(): SidebarItem[] {
|
||||
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
|
||||
children.forEach(child => child.parent = this);
|
||||
children.forEach((child) => (child.parent = this));
|
||||
return children;
|
||||
}
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
set activeWhen(regexp: string[]) {
|
||||
regexp.forEach(r => {
|
||||
regexp.forEach((r) => {
|
||||
this.activeMatchers.push(new RegExp(r));
|
||||
});
|
||||
}
|
||||
|
@ -110,7 +109,7 @@ export class SidebarItem extends LitElement {
|
|||
|
||||
onHashChange(): void {
|
||||
const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
||||
this.childItems.forEach(item => {
|
||||
this.childItems.forEach((item) => {
|
||||
this.expandParentRecursive(activePath, item);
|
||||
});
|
||||
this.isActive = this.matchesPath(activePath);
|
||||
|
@ -125,7 +124,7 @@ export class SidebarItem extends LitElement {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
return this.activeMatchers.some(v => {
|
||||
return this.activeMatchers.some((v) => {
|
||||
const match = v.exec(path);
|
||||
if (match !== null) {
|
||||
return true;
|
||||
|
@ -138,7 +137,7 @@ export class SidebarItem extends LitElement {
|
|||
item.parent.expanded = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
item.childItems.forEach(i => this.expandParentRecursive(activePath, i));
|
||||
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
@ -153,10 +152,16 @@ export class SidebarItem extends LitElement {
|
|||
}
|
||||
}
|
||||
if (this.childItems.length > 0) {
|
||||
return html`<li class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}">
|
||||
<button class="pf-c-nav__link" aria-expanded="true" @click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}>
|
||||
return html`<li
|
||||
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
|
||||
>
|
||||
<button
|
||||
class="pf-c-nav__link"
|
||||
aria-expanded="true"
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
>
|
||||
<slot name="label"></slot>
|
||||
<span class="pf-c-nav__toggle">
|
||||
<span class="pf-c-nav__toggle-icon">
|
||||
|
@ -172,15 +177,20 @@ export class SidebarItem extends LitElement {
|
|||
</li>`;
|
||||
}
|
||||
return html`<li class="pf-c-nav__item">
|
||||
${this.path ? html`
|
||||
<a href="${this.isAbsoluteLink ? "" : "#"}${this.path}" class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}">
|
||||
<slot name="label"></slot>
|
||||
</a>
|
||||
` : html`
|
||||
<span class="pf-c-nav__link">
|
||||
<slot name="label"></slot>
|
||||
</span>
|
||||
`}
|
||||
${this.path
|
||||
? html`
|
||||
<a
|
||||
href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
|
||||
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
|
||||
>
|
||||
<slot name="label"></slot>
|
||||
</a>
|
||||
`
|
||||
: html`
|
||||
<span class="pf-c-nav__link">
|
||||
<slot name="label"></slot>
|
||||
</span>
|
||||
`}
|
||||
</li>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
|||
|
||||
@customElement("ak-sidebar-user")
|
||||
export class SidebarUser extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
|
@ -34,9 +33,16 @@ export class SidebarUser extends LitElement {
|
|||
render(): TemplateResult {
|
||||
return html`
|
||||
<a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings">
|
||||
${until(me().then((u) => {
|
||||
return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`;
|
||||
}), html``)}
|
||||
${until(
|
||||
me().then((u) => {
|
||||
return html`<img
|
||||
class="pf-c-avatar"
|
||||
src="${ifDefined(u.user.avatar)}"
|
||||
alt=""
|
||||
/>`;
|
||||
}),
|
||||
html``,
|
||||
)}
|
||||
</a>
|
||||
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||
|
|
|
@ -20,7 +20,6 @@ import { EVENT_REFRESH } from "../../constants";
|
|||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
export class TableColumn {
|
||||
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
|
||||
|
@ -45,25 +44,27 @@ export class TableColumn {
|
|||
|
||||
private getSortIndicator(table: Table<unknown>): string {
|
||||
switch (table.order) {
|
||||
case this.orderBy:
|
||||
return "fa-long-arrow-alt-down";
|
||||
case `-${this.orderBy}`:
|
||||
return "fa-long-arrow-alt-up";
|
||||
default:
|
||||
return "fa-arrows-alt-v";
|
||||
case this.orderBy:
|
||||
return "fa-long-arrow-alt-down";
|
||||
case `-${this.orderBy}`:
|
||||
return "fa-long-arrow-alt-up";
|
||||
default:
|
||||
return "fa-arrows-alt-v";
|
||||
}
|
||||
}
|
||||
|
||||
renderSortable(table: Table<unknown>): TemplateResult {
|
||||
return html`
|
||||
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
|
||||
<div class="pf-c-table__button-content">
|
||||
<span class="pf-c-table__text">${this.title}</span>
|
||||
<span class="pf-c-table__sort-indicator">
|
||||
<i class="fas ${this.getSortIndicator(table)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
</button>`;
|
||||
return html` <button
|
||||
class="pf-c-table__button"
|
||||
@click=${() => this.headerClickHandler(table)}
|
||||
>
|
||||
<div class="pf-c-table__button-content">
|
||||
<span class="pf-c-table__text">${this.title}</span>
|
||||
<span class="pf-c-table__sort-indicator">
|
||||
<i class="fas ${this.getSortIndicator(table)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render(table: Table<unknown>): TemplateResult {
|
||||
|
@ -72,12 +73,14 @@ export class TableColumn {
|
|||
scope="col"
|
||||
class="
|
||||
${this.orderBy ? "pf-c-table__sort " : " "}
|
||||
${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""}
|
||||
">
|
||||
${table.order === this.orderBy || table.order === `-${this.orderBy}`
|
||||
? "pf-m-selected "
|
||||
: ""}
|
||||
"
|
||||
>
|
||||
${this.orderBy ? this.renderSortable(table) : html`${this.title}`}
|
||||
</th>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends LitElement {
|
||||
|
@ -99,32 +102,41 @@ export abstract class Table<T> extends LitElement {
|
|||
return html``;
|
||||
}
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
data?: AKResponse<T>;
|
||||
|
||||
@property({type: Number})
|
||||
@property({ type: Number })
|
||||
page = 1;
|
||||
|
||||
@property({type: String})
|
||||
@property({ type: String })
|
||||
order?: string;
|
||||
|
||||
@property({type: String})
|
||||
@property({ type: String })
|
||||
search?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
checkbox = false;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
selectedElements: T[] = [];
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
expandable = false;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
expandedRows: boolean[] = [];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFTable, PFBullseye, PFButton, PFToolbar, PFDropdown, PFPagination, AKGlobal];
|
||||
return [
|
||||
PFBase,
|
||||
PFTable,
|
||||
PFBullseye,
|
||||
PFButton,
|
||||
PFToolbar,
|
||||
PFDropdown,
|
||||
PFPagination,
|
||||
AKGlobal,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -139,24 +151,23 @@ export abstract class Table<T> extends LitElement {
|
|||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.apiEndpoint(this.page).then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
this.expandedRows = [];
|
||||
this.isLoading = false;
|
||||
}).catch(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
this.apiEndpoint(this.page)
|
||||
.then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
this.expandedRows = [];
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
private renderLoading(): TemplateResult {
|
||||
return html`<tr role="row">
|
||||
<td role="cell" colspan="25">
|
||||
<div class="pf-l-bullseye">
|
||||
<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>
|
||||
<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
@ -167,7 +178,11 @@ export abstract class Table<T> extends LitElement {
|
|||
<tr role="row">
|
||||
<td role="cell" colspan="8">
|
||||
<div class="pf-l-bullseye">
|
||||
${inner ? inner : html`<ak-empty-state header="${t`No objects found.`}"></ak-empty-state>`}
|
||||
${inner
|
||||
? inner
|
||||
: html`<ak-empty-state
|
||||
header="${t`No objects found.`}"
|
||||
></ak-empty-state>`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -182,40 +197,62 @@ export abstract class Table<T> extends LitElement {
|
|||
return [this.renderEmpty()];
|
||||
}
|
||||
return this.data.results.map((item: T, idx: number) => {
|
||||
if ((this.expandedRows.length - 1) < idx) {
|
||||
if (this.expandedRows.length - 1 < idx) {
|
||||
this.expandedRows[idx] = false;
|
||||
}
|
||||
return html`<tbody role="rowgroup" class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}">
|
||||
return html`<tbody
|
||||
role="rowgroup"
|
||||
class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}"
|
||||
>
|
||||
<tr role="row">
|
||||
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
|
||||
<input type="checkbox"
|
||||
?checked=${this.selectedElements.indexOf(item) >= 0}
|
||||
@input=${(ev: InputEvent) => {
|
||||
if ((ev.target as HTMLInputElement).checked) {
|
||||
// Add item to selected
|
||||
this.selectedElements.push(item);
|
||||
} else {
|
||||
// Get index of item and remove if selected
|
||||
const index = this.selectedElements.indexOf(item);
|
||||
if (index <= -1) return;
|
||||
this.selectedElements.splice(index, 1);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}} />
|
||||
</td>` : html``}
|
||||
${this.expandable ? html`<td class="pf-c-table__toggle" role="cell">
|
||||
<button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => {
|
||||
this.expandedRows[idx] = !this.expandedRows[idx];
|
||||
this.requestUpdate();
|
||||
}}>
|
||||
<div class="pf-c-table__toggle-icon"> <i class="fas fa-angle-down" aria-hidden="true"></i> </div>
|
||||
</button>
|
||||
</td>` : html``}
|
||||
${this.checkbox
|
||||
? html`<td class="pf-c-table__check" role="cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
?checked=${this.selectedElements.indexOf(item) >= 0}
|
||||
@input=${(ev: InputEvent) => {
|
||||
if ((ev.target as HTMLInputElement).checked) {
|
||||
// Add item to selected
|
||||
this.selectedElements.push(item);
|
||||
} else {
|
||||
// Get index of item and remove if selected
|
||||
const index = this.selectedElements.indexOf(item);
|
||||
if (index <= -1) return;
|
||||
this.selectedElements.splice(index, 1);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}}
|
||||
/>
|
||||
</td>`
|
||||
: html``}
|
||||
${this.expandable
|
||||
? html`<td class="pf-c-table__toggle" role="cell">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain ${this.expandedRows[idx]
|
||||
? "pf-m-expanded"
|
||||
: ""}"
|
||||
@click=${() => {
|
||||
this.expandedRows[idx] = !this.expandedRows[idx];
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
<div class="pf-c-table__toggle-icon">
|
||||
<i class="fas fa-angle-down" aria-hidden="true"></i
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
</td>`
|
||||
: html``}
|
||||
${this.row(item).map((col) => {
|
||||
return html`<td role="cell">${col}</td>`;
|
||||
})}
|
||||
</tr>
|
||||
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
|
||||
<tr
|
||||
class="pf-c-table__expandable-row ${this.expandedRows[idx]
|
||||
? "pf-m-expanded"
|
||||
: ""}"
|
||||
role="row"
|
||||
>
|
||||
<td></td>
|
||||
${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
|
||||
</tr>
|
||||
|
@ -230,10 +267,11 @@ export abstract class Table<T> extends LitElement {
|
|||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
class="pf-c-button pf-m-primary">
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
${t`Refresh`}
|
||||
</button>`;
|
||||
}
|
||||
|
@ -246,16 +284,20 @@ export abstract class Table<T> extends LitElement {
|
|||
if (!this.searchEnabled()) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
</ak-table-search> `;
|
||||
return html`<ak-table-search
|
||||
value=${ifDefined(this.search)}
|
||||
.onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
</ak-table-search
|
||||
> `;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
@ -268,20 +310,17 @@ export abstract class Table<T> extends LitElement {
|
|||
}
|
||||
|
||||
renderTable(): TemplateResult {
|
||||
return html`
|
||||
${this.checkbox ?
|
||||
html`<ak-chip-group>
|
||||
${this.selectedElements.map(el => {
|
||||
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
|
||||
})}
|
||||
</ak-chip-group>`:
|
||||
html``}
|
||||
return html` ${this.checkbox
|
||||
? html`<ak-chip-group>
|
||||
${this.selectedElements.map((el) => {
|
||||
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
|
||||
})}
|
||||
</ak-chip-group>`
|
||||
: html``}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
${this.renderSearch()}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
${this.renderToolbar()}
|
||||
</div>
|
||||
<div class="pf-c-toolbar__bulk-select">${this.renderToolbar()}</div>
|
||||
${this.renderToolbarAfter()}
|
||||
<ak-table-pagination
|
||||
class="pf-c-toolbar__item pf-m-pagination"
|
||||
|
@ -292,29 +331,36 @@ export abstract class Table<T> extends LitElement {
|
|||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
</ak-table-pagination>
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
|
||||
<input type="checkbox" aria-label=${t`Select all rows`} @input=${(ev: InputEvent) => {
|
||||
if ((ev.target as HTMLInputElement).checked) {
|
||||
this.selectedElements = this.data?.results || [];
|
||||
} else {
|
||||
this.selectedElements = [];
|
||||
}
|
||||
}} />
|
||||
</td>` : html``}
|
||||
${this.checkbox
|
||||
? html`<td class="pf-c-table__check" role="cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label=${t`Select all rows`}
|
||||
@input=${(ev: InputEvent) => {
|
||||
if ((ev.target as HTMLInputElement).checked) {
|
||||
this.selectedElements = this.data?.results || [];
|
||||
} else {
|
||||
this.selectedElements = [];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>`
|
||||
: html``}
|
||||
${this.expandable ? html`<td role="cell"></td>` : html``}
|
||||
${this.columns().map((col) => col.render(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
${(this.isLoading || !this.data) ? this.renderLoading() : this.renderRows()}
|
||||
${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
<ak-table-pagination
|
||||
|
@ -326,9 +372,10 @@ export abstract class Table<T> extends LitElement {
|
|||
new CustomEvent(EVENT_REFRESH, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
</ak-table-pagination>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,16 @@ export abstract class TableModal<T> extends Table<T> {
|
|||
open = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFModalBox, PFBullseye, PFContent, PFBackdrop, PFPage, PFStack, AKGlobal, MODAL_BUTTON_STYLES);
|
||||
return super.styles.concat(
|
||||
PFModalBox,
|
||||
PFBullseye,
|
||||
PFContent,
|
||||
PFBackdrop,
|
||||
PFPage,
|
||||
PFStack,
|
||||
AKGlobal,
|
||||
MODAL_BUTTON_STYLES,
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -33,7 +42,7 @@ export abstract class TableModal<T> extends Table<T> {
|
|||
}
|
||||
|
||||
resetForms(): void {
|
||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
|
||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
|
||||
if ("resetForm" in form) {
|
||||
form?.resetForm();
|
||||
}
|
||||
|
@ -42,7 +51,7 @@ export abstract class TableModal<T> extends Table<T> {
|
|||
|
||||
onClick(): void {
|
||||
this.open = true;
|
||||
this.querySelectorAll("*").forEach(child => {
|
||||
this.querySelectorAll("*").forEach((child) => {
|
||||
if ("requestUpdate" in child) {
|
||||
(child as LitElement).requestUpdate();
|
||||
}
|
||||
|
@ -56,11 +65,7 @@ export abstract class TableModal<T> extends Table<T> {
|
|||
renderModal(): TemplateResult {
|
||||
return html`<div class="pf-c-backdrop">
|
||||
<div class="pf-l-bullseye">
|
||||
<div
|
||||
class="pf-c-modal-box ${this.size}"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
|
||||
<button
|
||||
@click=${() => (this.open = false)}
|
||||
class="pf-c-button pf-m-plain"
|
||||
|
|
|
@ -19,7 +19,8 @@ export abstract class TablePage<T> extends Table<T> {
|
|||
return html`<ak-page-header
|
||||
icon=${this.pageIcon()}
|
||||
header=${this.pageTitle()}
|
||||
description=${ifDefined(this.pageDescription())}>
|
||||
description=${ifDefined(this.pageDescription())}
|
||||
>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">${this.renderTable()}</div>
|
||||
|
|
|
@ -8,12 +8,12 @@ import AKGlobal from "../../authentik.css";
|
|||
|
||||
@customElement("ak-table-pagination")
|
||||
export class TablePagination extends LitElement {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
pages?: AKPagination;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
// eslint-disable-next-line
|
||||
pageChangeHandler: (page: number) => void = (page: number) => {}
|
||||
pageChangeHandler: (page: number) => void = (page: number) => {};
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFButton, PFPagination, AKGlobal];
|
||||
|
@ -33,7 +33,9 @@ export class TablePagination extends LitElement {
|
|||
<div class="pf-c-pagination__nav-control pf-m-prev">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => { this.pageChangeHandler(this.pages?.previous || 0); }}
|
||||
@click=${() => {
|
||||
this.pageChangeHandler(this.pages?.previous || 0);
|
||||
}}
|
||||
?disabled="${(this.pages?.previous || 0) < 1}"
|
||||
aria-label="${t`Go to previous page`}"
|
||||
>
|
||||
|
@ -43,7 +45,9 @@ export class TablePagination extends LitElement {
|
|||
<div class="pf-c-pagination__nav-control pf-m-next">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => { this.pageChangeHandler(this.pages?.next || 0); }}
|
||||
@click=${() => {
|
||||
this.pageChangeHandler(this.pages?.next || 0);
|
||||
}}
|
||||
?disabled="${(this.pages?.next || 0) <= 0}"
|
||||
aria-label="${t`Go to next page`}"
|
||||
>
|
||||
|
|
|
@ -10,7 +10,6 @@ import { t } from "@lingui/macro";
|
|||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends LitElement {
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
|
@ -23,25 +22,36 @@ export class TableSearch extends LitElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form
|
||||
class="pf-c-input-group"
|
||||
method="GET"
|
||||
@submit=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.onSearch) return;
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
const el =
|
||||
this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
if (!el) return;
|
||||
if (el.value === "") return;
|
||||
this.onSearch(el?.value);
|
||||
}}>
|
||||
<input class="pf-c-form-control" name="search" type="search" placeholder=${t`Search...`} value="${ifDefined(this.value)}" @search=${(ev: Event) => {
|
||||
}}
|
||||
>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
name="search"
|
||||
type="search"
|
||||
placeholder=${t`Search...`}
|
||||
value="${ifDefined(this.value)}"
|
||||
@search=${(ev: Event) => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch((ev.target as HTMLInputElement).value);
|
||||
}}>
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}}
|
||||
/>
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
|||
|
||||
@customElement("ak-user-session-list")
|
||||
export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
||||
|
||||
@property()
|
||||
targetUser!: string;
|
||||
|
||||
|
@ -41,8 +40,7 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
|||
html`${item.userAgent.userAgent?.family}`,
|
||||
html`${item.userAgent.os?.family}`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`
|
||||
<ak-forms-delete
|
||||
html` <ak-forms-delete
|
||||
.obj=${item}
|
||||
objectLabel=${t`Session`}
|
||||
.usedBy=${() => {
|
||||
|
@ -54,12 +52,10 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
|||
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
|
||||
uuid: item.uuid || "",
|
||||
});
|
||||
}}>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete Session`}
|
||||
</button>
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Session`}</button>
|
||||
</ak-forms-delete>`,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,25 +36,22 @@ export class UserConsentList extends Table<UserConsent> {
|
|||
return [
|
||||
html`${item.application.name}`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`
|
||||
<ak-forms-delete
|
||||
html` <ak-forms-delete
|
||||
.obj=${item}
|
||||
objectLabel=${t`Consent`}
|
||||
.usedBy=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({
|
||||
id: item.pk
|
||||
id: item.pk,
|
||||
});
|
||||
}}
|
||||
.delete=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({
|
||||
id: item.pk,
|
||||
});
|
||||
}}>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${t`Delete Consent`}
|
||||
</button>
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Consent`}</button>
|
||||
</ak-forms-delete>`,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { t } from "@lingui/macro";
|
||||
import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
customElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
@ -26,7 +34,15 @@ import "./stages/password/PasswordStage";
|
|||
import "./stages/prompt/PromptStage";
|
||||
import "./sources/plex/PlexLoginInit";
|
||||
import { StageHost } from "./stages/base";
|
||||
import { ChallengeChoices, CurrentTenant, ChallengeTypes, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
|
||||
import {
|
||||
ChallengeChoices,
|
||||
CurrentTenant,
|
||||
ChallengeTypes,
|
||||
FlowChallengeResponseRequest,
|
||||
FlowsApi,
|
||||
RedirectChallenge,
|
||||
ShellChallenge,
|
||||
} from "authentik-api";
|
||||
import { DEFAULT_CONFIG, tenant } from "../api/Config";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
@ -37,13 +53,12 @@ import { WebsocketClient } from "../common/ws";
|
|||
|
||||
@customElement("ak-flow-executor")
|
||||
export class FlowExecutor extends LitElement implements StageHost {
|
||||
|
||||
flowSlug: string;
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
challenge?: ChallengeTypes;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
loading = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
|
@ -84,13 +99,15 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
}
|
||||
|
||||
setBackground(url: string): void {
|
||||
this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
|
||||
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
|
||||
});
|
||||
this.shadowRoot
|
||||
?.querySelectorAll<HTMLDivElement>(".pf-c-background-image")
|
||||
.forEach((bg) => {
|
||||
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
|
||||
});
|
||||
}
|
||||
|
||||
private postUpdate(): void {
|
||||
tenant().then(tenant => {
|
||||
tenant().then((tenant) => {
|
||||
if (this.challenge?.flowInfo?.title) {
|
||||
document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
|
||||
} else {
|
||||
|
@ -105,40 +122,48 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
// @ts-ignore
|
||||
payload.component = this.challenge.component;
|
||||
this.loading = true;
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
flowChallengeResponseRequest: payload,
|
||||
}).then((data) => {
|
||||
this.challenge = data;
|
||||
this.postUpdate();
|
||||
}).catch((e: Error | Response) => {
|
||||
this.errorMessage(e);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
return new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsExecutorSolve({
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
flowChallengeResponseRequest: payload,
|
||||
})
|
||||
.then((data) => {
|
||||
this.challenge = data;
|
||||
this.postUpdate();
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
this.errorMessage(e);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
configureSentry();
|
||||
tenant().then(tenant => this.tenant = tenant);
|
||||
tenant().then((tenant) => (this.tenant = tenant));
|
||||
this.loading = true;
|
||||
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
}).then((challenge) => {
|
||||
this.challenge = challenge;
|
||||
// Only set background on first update, flow won't change throughout execution
|
||||
if (this.challenge?.flowInfo?.background) {
|
||||
this.setBackground(this.challenge.flowInfo.background);
|
||||
}
|
||||
this.postUpdate();
|
||||
}).catch((e: Error | Response) => {
|
||||
// Catch JSON or Update errors
|
||||
this.errorMessage(e);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsExecutorGet({
|
||||
flowSlug: this.flowSlug,
|
||||
query: window.location.search.substring(1),
|
||||
})
|
||||
.then((challenge) => {
|
||||
this.challenge = challenge;
|
||||
// Only set background on first update, flow won't change throughout execution
|
||||
if (this.challenge?.flowInfo?.background) {
|
||||
this.setBackground(this.challenge.flowInfo.background);
|
||||
}
|
||||
this.postUpdate();
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
// Catch JSON or Update errors
|
||||
this.errorMessage(e);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
async errorMessage(error: Error | Response): Promise<void> {
|
||||
|
@ -167,7 +192,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>`
|
||||
</footer>`,
|
||||
} as ChallengeTypes;
|
||||
}
|
||||
|
||||
|
@ -183,46 +208,92 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
}
|
||||
switch (this.challenge.type) {
|
||||
case ChallengeChoices.Redirect:
|
||||
console.debug("authentik/flows: redirecting to url from server", (this.challenge as RedirectChallenge).to);
|
||||
console.debug(
|
||||
"authentik/flows: redirecting to url from server",
|
||||
(this.challenge as RedirectChallenge).to,
|
||||
);
|
||||
window.location.assign((this.challenge as RedirectChallenge).to);
|
||||
return html`<ak-empty-state
|
||||
?loading=${true}
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
case ChallengeChoices.Shell:
|
||||
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
|
||||
case ChallengeChoices.Native:
|
||||
switch (this.challenge.component) {
|
||||
case "ak-stage-access-denied":
|
||||
return html`<ak-stage-access-denied .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-access-denied>`;
|
||||
return html`<ak-stage-access-denied
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-access-denied>`;
|
||||
case "ak-stage-identification":
|
||||
return html`<ak-stage-identification .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-identification>`;
|
||||
return html`<ak-stage-identification
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-identification>`;
|
||||
case "ak-stage-password":
|
||||
return html`<ak-stage-password .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-password>`;
|
||||
return html`<ak-stage-password
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-password>`;
|
||||
case "ak-stage-captcha":
|
||||
return html`<ak-stage-captcha .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-captcha>`;
|
||||
return html`<ak-stage-captcha
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-captcha>`;
|
||||
case "ak-stage-consent":
|
||||
return html`<ak-stage-consent .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-consent>`;
|
||||
return html`<ak-stage-consent
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-consent>`;
|
||||
case "ak-stage-dummy":
|
||||
return html`<ak-stage-dummy .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-dummy>`;
|
||||
return html`<ak-stage-dummy
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-dummy>`;
|
||||
case "ak-stage-email":
|
||||
return html`<ak-stage-email .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-email>`;
|
||||
return html`<ak-stage-email
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-email>`;
|
||||
case "ak-stage-autosubmit":
|
||||
return html`<ak-stage-autosubmit .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-autosubmit>`;
|
||||
return html`<ak-stage-autosubmit
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-autosubmit>`;
|
||||
case "ak-stage-prompt":
|
||||
return html`<ak-stage-prompt .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-prompt>`;
|
||||
return html`<ak-stage-prompt
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-prompt>`;
|
||||
case "ak-stage-authenticator-totp":
|
||||
return html`<ak-stage-authenticator-totp .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-totp>`;
|
||||
return html`<ak-stage-authenticator-totp
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-totp>`;
|
||||
case "ak-stage-authenticator-duo":
|
||||
return html`<ak-stage-authenticator-duo .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-duo>`;
|
||||
return html`<ak-stage-authenticator-duo
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-duo>`;
|
||||
case "ak-stage-authenticator-static":
|
||||
return html`<ak-stage-authenticator-static .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-static>`;
|
||||
return html`<ak-stage-authenticator-static
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-static>`;
|
||||
case "ak-stage-authenticator-webauthn":
|
||||
return html`<ak-stage-authenticator-webauthn .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-webauthn>`;
|
||||
return html`<ak-stage-authenticator-webauthn
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-webauthn>`;
|
||||
case "ak-stage-authenticator-validate":
|
||||
return html`<ak-stage-authenticator-validate .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-validate>`;
|
||||
return html`<ak-stage-authenticator-validate
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-stage-authenticator-validate>`;
|
||||
case "ak-flow-sources-plex":
|
||||
return html`<ak-flow-sources-plex .host=${this as StageHost} .challenge=${this.challenge}></ak-flow-sources-plex>`;
|
||||
return html`<ak-flow-sources-plex
|
||||
.host=${this as StageHost}
|
||||
.challenge=${this.challenge}
|
||||
></ak-flow-sources-plex>`;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -236,59 +307,85 @@ export class FlowExecutor extends LitElement implements StageHost {
|
|||
|
||||
renderChallengeWrapper(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading=${true}
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`
|
||||
${this.loading ? this.renderLoading() : html``}
|
||||
${this.renderChallenge()}
|
||||
`;
|
||||
return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" />
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="pf-c-login">
|
||||
<div class="ak-login-container">
|
||||
<header class="pf-c-login__header">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="pf-c-login__main">
|
||||
${this.renderChallengeWrapper()}
|
||||
</div>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
${until(this.tenant?.uiFooterLinks?.map((link) => {
|
||||
return html`<li>
|
||||
<a href="${link.href || ""}">${link.name}</a>
|
||||
</li>`;
|
||||
}))}
|
||||
${this.tenant?.brandingTitle != "authentik" ? html`
|
||||
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
|
||||
` : html``}
|
||||
${this.challenge?.flowInfo?.background?.startsWith("/static") ? html`
|
||||
<li><a href="https://unsplash.com/@ventiviews">${t`Background image`}</a></li>
|
||||
` : html``}
|
||||
</ul>
|
||||
</footer>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="pf-c-background-image__filter"
|
||||
width="0"
|
||||
height="0"
|
||||
>
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0"
|
||||
/>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR
|
||||
type="table"
|
||||
tableValues="0.086274509803922 0.43921568627451"
|
||||
></feFuncR>
|
||||
<feFuncG
|
||||
type="table"
|
||||
tableValues="0.086274509803922 0.43921568627451"
|
||||
></feFuncG>
|
||||
<feFuncB
|
||||
type="table"
|
||||
tableValues="0.086274509803922 0.43921568627451"
|
||||
></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="pf-c-login">
|
||||
<div class="ak-login-container">
|
||||
<header class="pf-c-login__header">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img
|
||||
src="${ifDefined(this.tenant?.brandingLogo)}"
|
||||
alt="authentik icon"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
${until(
|
||||
this.tenant?.uiFooterLinks?.map((link) => {
|
||||
return html`<li>
|
||||
<a href="${link.href || ""}">${link.name}</a>
|
||||
</li>`;
|
||||
}),
|
||||
)}
|
||||
${this.tenant?.brandingTitle != "authentik"
|
||||
? html`
|
||||
<li>
|
||||
<a href="https://goauthentik.io"
|
||||
>${t`Powered by authentik`}</a
|
||||
>
|
||||
</li>
|
||||
`
|
||||
: html``}
|
||||
${this.challenge?.flowInfo?.background?.startsWith("/static")
|
||||
? html`
|
||||
<li>
|
||||
<a href="https://unsplash.com/@ventiviews"
|
||||
>${t`Background image`}</a
|
||||
>
|
||||
</li>
|
||||
`
|
||||
: html``}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import { t } from "@lingui/macro";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ak-form-static")
|
||||
export class FormStatic extends LitElement {
|
||||
|
||||
@property()
|
||||
userAvatar?: string;
|
||||
|
||||
|
@ -13,39 +20,45 @@ export class FormStatic extends LitElement {
|
|||
user = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFAvatar, css`
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFAvatar,
|
||||
css`
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="form-control-static">
|
||||
<div class="avatar">
|
||||
<img class="pf-c-avatar" src="${ifDefined(this.userAvatar)}" alt="${t`User's avatar`}">
|
||||
<img
|
||||
class="pf-c-avatar"
|
||||
src="${ifDefined(this.userAvatar)}"
|
||||
alt="${t`User's avatar`}"
|
||||
/>
|
||||
${this.user}
|
||||
</div>
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,23 +13,20 @@ import { t } from "@lingui/macro";
|
|||
import "../../elements/EmptyState";
|
||||
|
||||
@customElement("ak-stage-access-denied")
|
||||
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
|
||||
|
||||
export class FlowAccessDenied extends BaseStage<
|
||||
AccessDeniedChallenge,
|
||||
FlowChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form method="POST" class="pf-c-form">
|
||||
|
@ -39,15 +36,13 @@ export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChall
|
|||
${t`Request has been denied.`}
|
||||
</p>
|
||||
${this.challenge?.errorMessage &&
|
||||
html`<hr>
|
||||
html`<hr />
|
||||
<p>${this.challenge.errorMessage}</p>`}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,40 +23,54 @@ export const DEFAULT_HEADERS = {
|
|||
};
|
||||
|
||||
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
|
||||
const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
|
||||
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
||||
const top = (screen.height - h) / 4,
|
||||
left = (screen.width - w) / 2;
|
||||
const popup = window.open(
|
||||
url,
|
||||
title,
|
||||
`scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`,
|
||||
);
|
||||
return popup;
|
||||
}
|
||||
|
||||
export class PlexAPIClient {
|
||||
|
||||
token: string;
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
|
||||
const headers = { ...DEFAULT_HEADERS, ...{
|
||||
"X-Plex-Client-Identifier": clientIdentifier
|
||||
}};
|
||||
static async getPin(
|
||||
clientIdentifier: string,
|
||||
): Promise<{ authUrl: string; pin: PlexPinResponse }> {
|
||||
const headers = {
|
||||
...DEFAULT_HEADERS,
|
||||
...{
|
||||
"X-Plex-Client-Identifier": clientIdentifier,
|
||||
},
|
||||
};
|
||||
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
|
||||
method: "POST",
|
||||
headers: headers
|
||||
headers: headers,
|
||||
});
|
||||
const pin: PlexPinResponse = await pinResponse.json();
|
||||
return {
|
||||
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
|
||||
pin: pin
|
||||
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(
|
||||
clientIdentifier,
|
||||
)}&code=${pin.code}`,
|
||||
pin: pin,
|
||||
};
|
||||
}
|
||||
|
||||
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
|
||||
const headers = { ...DEFAULT_HEADERS, ...{
|
||||
"X-Plex-Client-Identifier": clientIdentifier
|
||||
}};
|
||||
const headers = {
|
||||
...DEFAULT_HEADERS,
|
||||
...{
|
||||
"X-Plex-Client-Identifier": clientIdentifier,
|
||||
},
|
||||
};
|
||||
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
|
||||
headers: headers
|
||||
headers: headers,
|
||||
});
|
||||
const pin: PlexPinResponse = await pinResponse.json();
|
||||
return pin.authToken || "";
|
||||
|
@ -65,7 +79,7 @@ export class PlexAPIClient {
|
|||
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
|
||||
const executePoll = async (
|
||||
resolve: (authToken: string) => void,
|
||||
reject: (e: Error) => void
|
||||
reject: (e: Error) => void,
|
||||
) => {
|
||||
try {
|
||||
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
|
||||
|
@ -84,13 +98,15 @@ export class PlexAPIClient {
|
|||
}
|
||||
|
||||
async getServers(): Promise<PlexResource[]> {
|
||||
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
|
||||
headers: DEFAULT_HEADERS
|
||||
});
|
||||
const resourcesResponse = await fetch(
|
||||
`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`,
|
||||
{
|
||||
headers: DEFAULT_HEADERS,
|
||||
},
|
||||
);
|
||||
const resources: PlexResource[] = await resourcesResponse.json();
|
||||
return resources.filter(r => {
|
||||
return resources.filter((r) => {
|
||||
return r.provides.toLowerCase().includes("server") && r.owned;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { t } from "@lingui/macro";
|
||||
import { PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest } from "authentik-api";
|
||||
import {
|
||||
PlexAuthenticationChallenge,
|
||||
PlexAuthenticationChallengeResponseRequest,
|
||||
} from "authentik-api";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
|
@ -16,10 +19,11 @@ import { SourcesApi } from "authentik-api";
|
|||
import { showMessage } from "../../../elements/messages/MessageContainer";
|
||||
import { MessageLevel } from "../../../elements/messages/Message";
|
||||
|
||||
|
||||
@customElement("ak-flow-sources-plex")
|
||||
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
|
||||
|
||||
export class PlexLoginInit extends BaseStage<
|
||||
PlexAuthenticationChallenge,
|
||||
PlexAuthenticationChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||
}
|
||||
|
@ -27,46 +31,43 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
|
|||
async firstUpdated(): Promise<void> {
|
||||
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
|
||||
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
|
||||
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => {
|
||||
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then((token) => {
|
||||
authWindow?.close();
|
||||
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
|
||||
plexTokenRedeemRequest: {
|
||||
plexToken: token,
|
||||
},
|
||||
slug: this.challenge?.slug || "",
|
||||
}).then((r) => {
|
||||
window.location.assign(r.to);
|
||||
}).catch((r: Response) => {
|
||||
r.json().then((body: {detail: string}) => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: body.detail
|
||||
new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesPlexRedeemTokenCreate({
|
||||
plexTokenRedeemRequest: {
|
||||
plexToken: token,
|
||||
},
|
||||
slug: this.challenge?.slug || "",
|
||||
})
|
||||
.then((r) => {
|
||||
window.location.assign(r.to);
|
||||
})
|
||||
.catch((r: Response) => {
|
||||
r.json().then((body: { detail: string }) => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: body.detail,
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.assign("/");
|
||||
}, 5000);
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.assign("/");
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${t`Authenticating with Plex...`}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Plex...`}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
<ak-empty-state
|
||||
?loading="${true}">
|
||||
</ak-empty-state>
|
||||
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,13 +11,19 @@ import { BaseStage } from "../base";
|
|||
import "../../../elements/forms/FormElement";
|
||||
import "../../../elements/EmptyState";
|
||||
import "../../FormStatic";
|
||||
import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, StagesApi } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorDuoChallenge,
|
||||
AuthenticatorDuoChallengeResponseRequest,
|
||||
StagesApi,
|
||||
} from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ak-stage-authenticator-duo")
|
||||
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
|
||||
|
||||
export class AuthenticatorDuoStage extends BaseStage<
|
||||
AuthenticatorDuoChallenge,
|
||||
AuthenticatorDuoChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
}
|
||||
|
@ -31,35 +37,41 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
|
|||
}
|
||||
|
||||
checkEnrollStatus(): Promise<void> {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
|
||||
stageUuid: this.challenge?.stageUuid || "",
|
||||
}).then(() => {
|
||||
this.host?.submit({});
|
||||
}).catch(() => {
|
||||
console.debug("authentik/flows/duo: Waiting for auth status");
|
||||
});
|
||||
return new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesAuthenticatorDuoEnrollmentStatusCreate({
|
||||
stageUuid: this.challenge?.stageUuid || "",
|
||||
})
|
||||
.then(() => {
|
||||
this.host?.submit({});
|
||||
})
|
||||
.catch(() => {
|
||||
console.debug("authentik/flows/duo: Waiting for auth status");
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<img src=${this.challenge.activationBarcode} />
|
||||
|
@ -69,18 +81,20 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
|
|||
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
||||
this.checkEnrollStatus();
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
@click=${() => {
|
||||
this.checkEnrollStatus();
|
||||
}}
|
||||
>
|
||||
${t`Check status`}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,58 +11,75 @@ import { BaseStage } from "../base";
|
|||
import "../../../elements/forms/FormElement";
|
||||
import "../../../elements/EmptyState";
|
||||
import "../../FormStatic";
|
||||
import { AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorStaticChallenge,
|
||||
AuthenticatorStaticChallengeResponseRequest,
|
||||
} from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
export const STATIC_TOKEN_STYLE = css`
|
||||
/* Static OTP Tokens */
|
||||
.ak-otp-tokens {
|
||||
list-style: circle;
|
||||
columns: 2;
|
||||
-webkit-columns: 2;
|
||||
-moz-columns: 2;
|
||||
margin-left: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.ak-otp-tokens li {
|
||||
font-size: var(--pf-global--FontSize--2xl);
|
||||
font-family: monospace;
|
||||
}
|
||||
/* Static OTP Tokens */
|
||||
.ak-otp-tokens {
|
||||
list-style: circle;
|
||||
columns: 2;
|
||||
-webkit-columns: 2;
|
||||
-moz-columns: 2;
|
||||
margin-left: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.ak-otp-tokens li {
|
||||
font-size: var(--pf-global--FontSize--2xl);
|
||||
font-family: monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@customElement("ak-stage-authenticator-static")
|
||||
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
|
||||
|
||||
export class AuthenticatorStaticStage extends BaseStage<
|
||||
AuthenticatorStaticChallenge,
|
||||
AuthenticatorStaticChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal, STATIC_TOKEN_STYLE];
|
||||
return [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
AKGlobal,
|
||||
STATIC_TOKEN_STYLE,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-form-element
|
||||
label="${t`Tokens`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group">
|
||||
class="pf-c-form__group"
|
||||
>
|
||||
<ul class="ak-otp-tokens">
|
||||
${this.challenge.codes.map((token) => {
|
||||
return html`<li>${token}</li>`;
|
||||
|
@ -78,9 +95,7 @@ export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChall
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,53 +14,66 @@ import { showMessage } from "../../../elements/messages/MessageContainer";
|
|||
import "../../../elements/EmptyState";
|
||||
import "../../FormStatic";
|
||||
import { MessageLevel } from "../../../elements/messages/Message";
|
||||
import { AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorTOTPChallenge,
|
||||
AuthenticatorTOTPChallengeResponseRequest,
|
||||
} from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
|
||||
@customElement("ak-stage-authenticator-totp")
|
||||
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
|
||||
|
||||
export class AuthenticatorTOTPStage extends BaseStage<
|
||||
AuthenticatorTOTPChallenge,
|
||||
AuthenticatorTOTPChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
||||
<ak-form-element>
|
||||
<!-- @ts-ignore -->
|
||||
<qr-code data="${this.challenge.configUrl}"></qr-code>
|
||||
<button type="button" class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress" @click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.challenge?.configUrl) return;
|
||||
navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: t`Successfully copied TOTP Config.`
|
||||
});
|
||||
});
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress"
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.challenge?.configUrl) return;
|
||||
navigator.clipboard
|
||||
.writeText(this.challenge?.configUrl)
|
||||
.then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: t`Successfully copied TOTP Config.`,
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
|
||||
${t`Copy`}
|
||||
</button>
|
||||
|
@ -69,9 +82,11 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
|||
label="${t`Code`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}>
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input type="text"
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
|
@ -79,7 +94,8 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
|||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
required>
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
@ -90,9 +106,7 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,11 @@ import "./AuthenticatorValidateStageWebAuthn";
|
|||
import "./AuthenticatorValidateStageCode";
|
||||
import "./AuthenticatorValidateStageDuo";
|
||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest,
|
||||
DeviceChallenge,
|
||||
} from "authentik-api";
|
||||
|
||||
export enum DeviceClasses {
|
||||
STATIC = "static",
|
||||
|
@ -22,9 +26,14 @@ export enum DeviceClasses {
|
|||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-validate")
|
||||
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
|
||||
|
||||
@property({attribute: false})
|
||||
export class AuthenticatorValidateStage
|
||||
extends BaseStage<
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest
|
||||
>
|
||||
implements StageHost
|
||||
{
|
||||
@property({ attribute: false })
|
||||
selectedDeviceChallenge?: DeviceChallenge;
|
||||
|
||||
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
|
||||
|
@ -65,7 +74,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
|||
return html`<i class="fas fa-mobile-alt"></i>
|
||||
<div class="right">
|
||||
<p>${t`Duo push-notifications`}</p>
|
||||
<small>${t`Receive a push notification on your phone to prove your identity.`}</small>
|
||||
<small
|
||||
>${t`Receive a push notification on your phone to prove your identity.`}</small
|
||||
>
|
||||
</div>`;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<i class="fas fa-mobile-alt"></i>
|
||||
|
@ -78,7 +89,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
|||
// and we have a pre-filled value from the password manager,
|
||||
// directly set the the TOTP device Challenge as active.
|
||||
if (PasswordManagerPrefill.totp) {
|
||||
console.debug("authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge");
|
||||
console.debug(
|
||||
"authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
|
||||
);
|
||||
this.selectedDeviceChallenge = deviceChallenge;
|
||||
// Delay the update as a re-render isn't triggered from here
|
||||
setTimeout(() => {
|
||||
|
@ -103,13 +116,16 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
|||
}
|
||||
|
||||
renderDevicePicker(): TemplateResult {
|
||||
return html`
|
||||
<ul>
|
||||
return html` <ul>
|
||||
${this.challenge?.deviceChallenges.map((challenges) => {
|
||||
return html`<li>
|
||||
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
|
||||
this.selectedDeviceChallenge = challenges;
|
||||
}}>
|
||||
<button
|
||||
class="pf-c-button authenticator-button"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.selectedDeviceChallenge = challenges;
|
||||
}}
|
||||
>
|
||||
${this.renderDevicePickerSingle(challenges)}
|
||||
</button>
|
||||
</li>`;
|
||||
|
@ -122,60 +138,56 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
|||
return html``;
|
||||
}
|
||||
switch (this.selectedDeviceChallenge?.deviceClass) {
|
||||
case DeviceClasses.STATIC:
|
||||
case DeviceClasses.TOTP:
|
||||
return html`<ak-stage-authenticator-validate-code
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
||||
</ak-stage-authenticator-validate-code>`;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<ak-stage-authenticator-validate-webauthn
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
||||
</ak-stage-authenticator-validate-webauthn>`;
|
||||
case DeviceClasses.DUO:
|
||||
return html`<ak-stage-authenticator-validate-duo
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
||||
</ak-stage-authenticator-validate-duo>`;
|
||||
case DeviceClasses.STATIC:
|
||||
case DeviceClasses.TOTP:
|
||||
return html`<ak-stage-authenticator-validate-code
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||
>
|
||||
</ak-stage-authenticator-validate-code>`;
|
||||
case DeviceClasses.WEBAUTHN:
|
||||
return html`<ak-stage-authenticator-validate-webauthn
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||
>
|
||||
</ak-stage-authenticator-validate-webauthn>`;
|
||||
case DeviceClasses.DUO:
|
||||
return html`<ak-stage-authenticator-validate-duo
|
||||
.host=${this}
|
||||
.challenge=${this.challenge}
|
||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||
>
|
||||
</ak-stage-authenticator-validate-duo>`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
// User only has a single device class, so we don't show a picker
|
||||
if (this.challenge?.deviceChallenges.length === 1) {
|
||||
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
|
||||
${t`Select an identification method.`}
|
||||
</p>`}
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
${this.selectedDeviceChallenge
|
||||
? ""
|
||||
: html`<p class="pf-c-login__main-header-desc">
|
||||
${t`Select an identification method.`}
|
||||
</p>`}
|
||||
</header>
|
||||
${this.selectedDeviceChallenge ?
|
||||
this.renderDeviceChallenge() :
|
||||
html`<div class="pf-c-login__main-body">
|
||||
${this.renderDevicePicker()}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
</footer>`}`;
|
||||
${this.selectedDeviceChallenge
|
||||
? this.renderDeviceChallenge()
|
||||
: html`<div class="pf-c-login__main-body">${this.renderDevicePicker()}</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,12 +13,18 @@ import "../../../elements/forms/FormElement";
|
|||
import "../../../elements/EmptyState";
|
||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
||||
import "../../FormStatic";
|
||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest,
|
||||
DeviceChallenge,
|
||||
} from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ak-stage-authenticator-validate-code")
|
||||
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
||||
|
||||
export class AuthenticatorValidateStageWebCode extends BaseStage<
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest
|
||||
> {
|
||||
@property({ attribute: false })
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
|
@ -31,60 +37,72 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
|
|||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-form-element
|
||||
label="${t`Code`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}>
|
||||
<!-- @ts-ignore -->
|
||||
<input type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="${t`Please enter your TOTP Code`}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
value="${PasswordManagerPrefill.totp || ""}"
|
||||
required>
|
||||
</ak-form-element>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<ak-form-element
|
||||
label="${t`Code`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="${t`Please enter your TOTP Code`}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
value="${PasswordManagerPrefill.totp || ""}"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton ?
|
||||
html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
||||
if (!this.host) return;
|
||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
||||
}}>
|
||||
${t`Return to device picker`}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
</button>
|
||||
</li>`:
|
||||
html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton
|
||||
? html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
@click=${() => {
|
||||
if (!this.host) return;
|
||||
(
|
||||
this.host as AuthenticatorValidateStage
|
||||
).selectedDeviceChallenge = undefined;
|
||||
}}
|
||||
>
|
||||
${t`Return to device picker`}
|
||||
</button>
|
||||
</li>`
|
||||
: html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,12 +12,18 @@ import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
|
|||
import "../../../elements/forms/FormElement";
|
||||
import "../../../elements/EmptyState";
|
||||
import "../../FormStatic";
|
||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest,
|
||||
DeviceChallenge,
|
||||
} from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ak-stage-authenticator-validate-duo")
|
||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
||||
|
||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest
|
||||
> {
|
||||
@property({ attribute: false })
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
|
@ -30,49 +36,58 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
|
|||
|
||||
firstUpdated(): void {
|
||||
this.host?.submit({
|
||||
"duo": this.deviceChallenge?.deviceUid
|
||||
duo: this.deviceChallenge?.deviceUid,
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton ?
|
||||
html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
||||
if (!this.host) return;
|
||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
||||
}}>
|
||||
${t`Return to device picker`}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
</button>
|
||||
</li>`:
|
||||
html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton
|
||||
? html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
@click=${() => {
|
||||
if (!this.host) return;
|
||||
(
|
||||
this.host as AuthenticatorValidateStage
|
||||
).selectedDeviceChallenge = undefined;
|
||||
}}
|
||||
>
|
||||
${t`Return to device picker`}
|
||||
</button>
|
||||
</li>`
|
||||
: html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,15 +8,24 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import AKGlobal from "../../../authentik.css";
|
||||
import { PFSize } from "../../../elements/Spinner";
|
||||
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
|
||||
import {
|
||||
transformAssertionForServer,
|
||||
transformCredentialRequestOptions,
|
||||
} from "../authenticator_webauthn/utils";
|
||||
import { BaseStage } from "../base";
|
||||
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
|
||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest,
|
||||
DeviceChallenge,
|
||||
} from "authentik-api";
|
||||
|
||||
@customElement("ak-stage-authenticator-validate-webauthn")
|
||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
||||
|
||||
@property({attribute: false})
|
||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
||||
AuthenticatorValidationChallenge,
|
||||
AuthenticatorValidationChallengeResponseRequest
|
||||
> {
|
||||
@property({ attribute: false })
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
@property({ type: Boolean })
|
||||
|
@ -25,7 +34,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
|||
@property()
|
||||
authenticateMessage = "";
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
showBackButton = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -35,8 +44,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
|||
async authenticate(): Promise<void> {
|
||||
// convert certain members of the PublicKeyCredentialRequestOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
|
||||
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
|
||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>(
|
||||
this.deviceChallenge?.challenge
|
||||
);
|
||||
const transformedCredentialRequestOptions =
|
||||
transformCredentialRequestOptions(credentialRequestOptions);
|
||||
|
||||
// request the authenticator to create an assertion signature using the
|
||||
// credential private key
|
||||
|
@ -54,12 +66,14 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
|||
|
||||
// we now have an authentication assertion! encode the byte arrays contained
|
||||
// in the assertion data as strings for posting to the server
|
||||
const transformedAssertionForServer = transformAssertionForServer(<PublicKeyCredential>assertion);
|
||||
const transformedAssertionForServer = transformAssertionForServer(
|
||||
<PublicKeyCredential>assertion,
|
||||
);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
try {
|
||||
await this.host?.submit({
|
||||
webauthn: transformedAssertionForServer
|
||||
webauthn: transformedAssertionForServer,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(t`Error when validating assertion on server: ${err}`);
|
||||
|
@ -75,48 +89,56 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
|||
return;
|
||||
}
|
||||
this.authenticateRunning = true;
|
||||
this.authenticate().catch((e) => {
|
||||
console.error(e);
|
||||
this.authenticateMessage = e.toString();
|
||||
}).finally(() => {
|
||||
this.authenticateRunning = false;
|
||||
});
|
||||
this.authenticate()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.authenticateMessage = e.toString();
|
||||
})
|
||||
.finally(() => {
|
||||
this.authenticateRunning = false;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-login__main-body">
|
||||
${this.authenticateRunning ?
|
||||
html`<div class="pf-c-empty-state__content">
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-l-bullseye__item">
|
||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>`:
|
||||
html`
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<p class="pf-m-block">${this.authenticateMessage}</p>
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
||||
this.authenticateWrapper();
|
||||
}}>
|
||||
${t`Retry authentication`}
|
||||
</button>
|
||||
</div>`}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton ?
|
||||
html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
||||
if (!this.host) return;
|
||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
||||
}}>
|
||||
${t`Return to device picker`}
|
||||
</button>
|
||||
</li>`:
|
||||
html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
${this.authenticateRunning
|
||||
? html`<div class="pf-c-empty-state__content">
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-l-bullseye__item">
|
||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: html` <div class="pf-c-form__group pf-m-action">
|
||||
<p class="pf-m-block">${this.authenticateMessage}</p>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
@click=${() => {
|
||||
this.authenticateWrapper();
|
||||
}}
|
||||
>
|
||||
${t`Retry authentication`}
|
||||
</button>
|
||||
</div>`}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
${this.showBackButton
|
||||
? html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
@click=${() => {
|
||||
if (!this.host) return;
|
||||
(
|
||||
this.host as AuthenticatorValidateStage
|
||||
).selectedDeviceChallenge = undefined;
|
||||
}}
|
||||
>
|
||||
${t`Return to device picker`}
|
||||
</button>
|
||||
</li>`
|
||||
: html``}
|
||||
</ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,17 +9,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||
import AKGlobal from "../../../authentik.css";
|
||||
import { PFSize } from "../../../elements/Spinner";
|
||||
import { BaseStage } from "../base";
|
||||
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
|
||||
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
|
||||
import {
|
||||
Assertion,
|
||||
transformCredentialCreateOptions,
|
||||
transformNewAssertionForServer,
|
||||
} from "./utils";
|
||||
import {
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
AuthenticatorWebAuthnChallengeResponseRequest,
|
||||
} from "authentik-api";
|
||||
|
||||
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
|
||||
response: Assertion;
|
||||
}
|
||||
|
||||
@customElement("ak-stage-authenticator-webauthn")
|
||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
|
||||
|
||||
@property({type: Boolean})
|
||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
AuthenticatorWebAuthnChallengeResponseRequest
|
||||
> {
|
||||
@property({ type: Boolean })
|
||||
registerRunning = false;
|
||||
|
||||
@property()
|
||||
|
@ -35,13 +44,15 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
|||
}
|
||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||
// byte arrays as expected by the spec.
|
||||
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration as PublicKeyCredentialCreationOptions);
|
||||
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
|
||||
this.challenge?.registration as PublicKeyCredentialCreationOptions,
|
||||
);
|
||||
|
||||
// request the authenticator(s) to create a new credential keypair.
|
||||
let credential;
|
||||
try {
|
||||
credential = <PublicKeyCredential> await navigator.credentials.create({
|
||||
publicKey: publicKeyCredentialCreateOptions
|
||||
credential = <PublicKeyCredential>await navigator.credentials.create({
|
||||
publicKey: publicKeyCredentialCreateOptions,
|
||||
});
|
||||
if (!credential) {
|
||||
throw new Error("Credential is empty");
|
||||
|
@ -58,7 +69,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
|||
// and storing the public key
|
||||
try {
|
||||
await this.host?.submit({
|
||||
response: newAssertionForServer
|
||||
response: newAssertionForServer,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(t`Server validation of credential failed: ${err}`);
|
||||
|
@ -70,12 +81,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
|||
return;
|
||||
}
|
||||
this.registerRunning = true;
|
||||
this.register().catch((e) => {
|
||||
console.error(e);
|
||||
this.registerMessage = e.toString();
|
||||
}).finally(() => {
|
||||
this.registerRunning = false;
|
||||
});
|
||||
this.register()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.registerMessage = e.toString();
|
||||
})
|
||||
.finally(() => {
|
||||
this.registerRunning = false;
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
|
@ -89,26 +102,32 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
|||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
${this.registerRunning ?
|
||||
html`<div class="pf-c-empty-state__content">
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-l-bullseye__item">
|
||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>`:
|
||||
html`
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
${this.challenge?.responseErrors ?
|
||||
html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
|
||||
html``}
|
||||
<p class="pf-m-block">${this.registerMessage}</p>
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
||||
this.registerWrapper();
|
||||
}}>
|
||||
${t`Register device`}
|
||||
</button>
|
||||
</div>`}
|
||||
${
|
||||
this.registerRunning
|
||||
? html`<div class="pf-c-empty-state__content">
|
||||
<div class="pf-l-bullseye">
|
||||
<div class="pf-l-bullseye__item">
|
||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: html` <div class="pf-c-form__group pf-m-action">
|
||||
${this.challenge?.responseErrors
|
||||
? html`<p class="pf-m-block">
|
||||
${this.challenge.responseErrors["response"][0].string}
|
||||
</p>`
|
||||
: html``}
|
||||
<p class="pf-m-block">${this.registerMessage}</p>
|
||||
<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
@click=${() => {
|
||||
this.registerWrapper();
|
||||
}}
|
||||
>
|
||||
${t`Register device`}
|
||||
</button>
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
|
@ -116,5 +135,4 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
|||
</ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,30 +2,28 @@ import * as base64js from "base64-js";
|
|||
import { hexEncode } from "../../../utils";
|
||||
|
||||
export function b64enc(buf: Uint8Array): string {
|
||||
return base64js.fromByteArray(buf)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
||||
export function b64RawEnc(buf: Uint8Array): string {
|
||||
return base64js.fromByteArray(buf)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms items in the credentialCreateOptions generated on the server
|
||||
* into byte arrays expected by the navigator.credentials.create() call
|
||||
*/
|
||||
export function transformCredentialCreateOptions(credentialCreateOptions: PublicKeyCredentialCreationOptions): PublicKeyCredentialCreationOptions {
|
||||
export function transformCredentialCreateOptions(
|
||||
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||
): PublicKeyCredentialCreationOptions {
|
||||
const user = credentialCreateOptions.user;
|
||||
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
|
||||
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
||||
|
||||
const transformedCredentialCreateOptions = Object.assign(
|
||||
{}, credentialCreateOptions,
|
||||
{ challenge, user });
|
||||
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
|
||||
challenge,
|
||||
user,
|
||||
});
|
||||
|
||||
return transformedCredentialCreateOptions;
|
||||
}
|
||||
|
@ -46,11 +44,10 @@ export interface Assertion {
|
|||
*/
|
||||
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||
const attObj = new Uint8Array(
|
||||
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject);
|
||||
const clientDataJSON = new Uint8Array(
|
||||
newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(
|
||||
newAssertion.rawId);
|
||||
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject,
|
||||
);
|
||||
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
return {
|
||||
|
@ -59,26 +56,32 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
|
|||
type: newAssertion.type,
|
||||
attObj: b64enc(attObj),
|
||||
clientData: b64enc(clientDataJSON),
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
};
|
||||
}
|
||||
|
||||
function u8arr(input: string): Uint8Array {
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
|
||||
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
}
|
||||
|
||||
export function transformCredentialRequestOptions(credentialRequestOptions: PublicKeyCredentialRequestOptions): PublicKeyCredentialRequestOptions {
|
||||
export function transformCredentialRequestOptions(
|
||||
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
const challenge = u8arr(credentialRequestOptions.challenge.toString());
|
||||
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(credentialDescriptor => {
|
||||
const id = u8arr(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
});
|
||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||
(credentialDescriptor) => {
|
||||
const id = u8arr(credentialDescriptor.id.toString());
|
||||
return Object.assign({}, credentialDescriptor, { id });
|
||||
},
|
||||
);
|
||||
|
||||
const transformedCredentialRequestOptions = Object.assign(
|
||||
{},
|
||||
credentialRequestOptions,
|
||||
{ challenge, allowCredentials });
|
||||
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
|
||||
challenge,
|
||||
allowCredentials,
|
||||
});
|
||||
|
||||
return transformedCredentialRequestOptions;
|
||||
}
|
||||
|
@ -97,8 +100,8 @@ export interface AuthAssertion {
|
|||
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||
* @param {PublicKeyCredential} newAssertion
|
||||
*/
|
||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion{
|
||||
const response = <AuthenticatorAssertionResponse> newAssertion.response;
|
||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||
const response = <AuthenticatorAssertionResponse>newAssertion.response;
|
||||
const authData = new Uint8Array(response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||
const rawId = new Uint8Array(newAssertion.rawId);
|
||||
|
@ -112,6 +115,6 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
|
|||
authData: b64RawEnc(authData),
|
||||
clientData: b64RawEnc(clientDataJSON),
|
||||
signature: hexEncode(sig),
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,36 +12,37 @@ import "../../../elements/EmptyState";
|
|||
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
|
||||
|
||||
@customElement("ak-stage-autosubmit")
|
||||
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
|
||||
|
||||
export class AutosubmitStage extends BaseStage<
|
||||
AutosubmitChallenge,
|
||||
AutoSubmitChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||
}
|
||||
|
||||
updated(): void {
|
||||
this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit();});
|
||||
this.shadowRoot?.querySelectorAll("form").forEach((form) => {
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
|
||||
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
|
||||
return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
|
||||
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||
return html`<input
|
||||
type="hidden"
|
||||
name="${key as string}"
|
||||
value="${value as string}"
|
||||
/>`;
|
||||
})}
|
||||
<ak-empty-state
|
||||
?loading="${true}">
|
||||
</ak-empty-state>
|
||||
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
|
@ -50,9 +51,7 @@ export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitCh
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ export interface StageHost {
|
|||
}
|
||||
|
||||
export class BaseStage<Tin, Tout> extends LitElement {
|
||||
|
||||
host!: StageHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
|
@ -19,7 +18,7 @@ export class BaseStage<Tin, Tout> extends LitElement {
|
|||
[key: string]: unknown;
|
||||
} = {};
|
||||
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
||||
form.forEach((value, key) => object[key] = value);
|
||||
form.forEach((value, key) => (object[key] = value));
|
||||
this.host?.submit(object as unknown as Tout);
|
||||
}
|
||||
|
||||
|
@ -28,18 +27,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
|
|||
return html``;
|
||||
}
|
||||
return html`<div class="pf-c-form__alert">
|
||||
${errors.map(err => {
|
||||
${errors.map((err) => {
|
||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">
|
||||
${err.string}
|
||||
</h4>
|
||||
<h4 class="pf-c-alert__title">${err.string}</h4>
|
||||
</div>`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
|||
|
||||
@customElement("ak-stage-captcha")
|
||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||
sitekey: this.challenge.siteKey,
|
||||
callback: (token) => {
|
||||
this.host?.submit({
|
||||
"token": token,
|
||||
token: token,
|
||||
});
|
||||
},
|
||||
size: "invisible",
|
||||
|
@ -51,24 +50,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form">
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="ak-loading">
|
||||
|
@ -77,9 +74,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,48 +15,63 @@ import "../../FormStatic";
|
|||
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
|
||||
@customElement("ak-stage-consent")
|
||||
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
return [
|
||||
PFBase,
|
||||
PFLogin,
|
||||
PFList,
|
||||
PFForm,
|
||||
PFSpacing,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
AKGlobal,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<div class="pf-c-form__group">
|
||||
<p id="header-text" class="pf-u-mb-xl">
|
||||
${this.challenge.headerText}
|
||||
</p>
|
||||
${this.challenge.permissions.length > 0 ? html`
|
||||
<p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
|
||||
<ul class="pf-c-list" id="permmissions">
|
||||
${this.challenge.permissions.map((permission) => {
|
||||
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
|
||||
})}
|
||||
</ul>
|
||||
` : html``}
|
||||
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
|
||||
${this.challenge.permissions.length > 0
|
||||
? html`
|
||||
<p class="pf-u-mb-sm">
|
||||
${t`Application requires following permissions:`}
|
||||
</p>
|
||||
<ul class="pf-c-list" id="permmissions">
|
||||
${this.challenge.permissions.map((permission) => {
|
||||
return html`<li data-permission-code="${permission.id}">
|
||||
${permission.name}
|
||||
</li>`;
|
||||
})}
|
||||
</ul>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
@ -67,9 +82,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,25 +14,24 @@ import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
|
|||
|
||||
@customElement("ak-stage-dummy")
|
||||
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
|
@ -41,9 +40,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,29 +13,26 @@ import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
|
|||
|
||||
@customElement("ak-stage-email")
|
||||
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
${t`Check your Emails for a password reset link.`}
|
||||
</p>
|
||||
<p>${t`Check your Emails for a password reset link.`}</p>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
|
@ -46,9 +43,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,7 +11,12 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
|||
import AKGlobal from "../../../authentik.css";
|
||||
import "../../../elements/forms/FormElement";
|
||||
import "../../../elements/EmptyState";
|
||||
import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api";
|
||||
import {
|
||||
IdentificationChallenge,
|
||||
IdentificationChallengeResponseRequest,
|
||||
LoginSource,
|
||||
UserFieldsEnum,
|
||||
} from "authentik-api";
|
||||
|
||||
export const PasswordManagerPrefill: {
|
||||
password: string | undefined;
|
||||
|
@ -21,13 +26,27 @@ export const PasswordManagerPrefill: {
|
|||
totp: undefined,
|
||||
};
|
||||
|
||||
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", { style: "short", type: "disjunction" });
|
||||
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", {
|
||||
style: "short",
|
||||
type: "disjunction",
|
||||
});
|
||||
|
||||
@customElement("ak-stage-identification")
|
||||
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
|
||||
|
||||
export class IdentificationStage extends BaseStage<
|
||||
IdentificationChallenge,
|
||||
IdentificationChallengeResponseRequest
|
||||
> {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
|
||||
return [
|
||||
PFBase,
|
||||
PFAlert,
|
||||
PFLogin,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFTitle,
|
||||
PFButton,
|
||||
AKGlobal,
|
||||
].concat(
|
||||
css`
|
||||
/* login page's icons */
|
||||
.pf-c-login__main-footer-links-item button {
|
||||
|
@ -41,7 +60,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
height: 100%;
|
||||
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
||||
}
|
||||
`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -56,12 +75,14 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
username.setAttribute("autocomplete", "username");
|
||||
username.onkeyup = (ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
||||
input.value = el.value;
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
(this.shadowRoot || this)
|
||||
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||
.forEach((input) => {
|
||||
input.value = el.value;
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
wrapperForm.appendChild(username);
|
||||
const password = document.createElement("input");
|
||||
|
@ -79,11 +100,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
PasswordManagerPrefill.password = el.value;
|
||||
// Because password managers fill username, then password,
|
||||
// we need to re-focus the uid_field here too
|
||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
(this.shadowRoot || this)
|
||||
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||
.forEach((input) => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
wrapperForm.appendChild(password);
|
||||
const totp = document.createElement("input");
|
||||
|
@ -101,11 +124,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
PasswordManagerPrefill.totp = el.value;
|
||||
// Because totp managers fill username, then password, then optionally,
|
||||
// we need to re-focus the uid_field here too
|
||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
(this.shadowRoot || this)
|
||||
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||
.forEach((input) => {
|
||||
// Because we assume only one input field exists that matches this
|
||||
// call focus so the user can press enter
|
||||
input.focus();
|
||||
});
|
||||
};
|
||||
wrapperForm.appendChild(totp);
|
||||
}
|
||||
|
@ -113,16 +138,19 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
renderSource(source: LoginSource): TemplateResult {
|
||||
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
|
||||
if (source.iconUrl) {
|
||||
icon = html`<img src="${source.iconUrl}" alt="${source.name}">`;
|
||||
icon = html`<img src="${source.iconUrl}" alt="${source.name}" />`;
|
||||
}
|
||||
return html`<li class="pf-c-login__main-footer-links-item">
|
||||
<button type="button" @click=${() => {
|
||||
<button
|
||||
type="button"
|
||||
@click=${() => {
|
||||
if (!this.host) return;
|
||||
this.host.challenge = source.challenge;
|
||||
}}>
|
||||
${icon}
|
||||
</button>
|
||||
</li>`;
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
</button>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
renderFooter(): TemplateResult {
|
||||
|
@ -130,24 +158,26 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
return html``;
|
||||
}
|
||||
return html`<div class="pf-c-login__main-footer-band">
|
||||
${this.challenge.enrollUrl ? html`
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
${t`Need an account?`}
|
||||
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
|
||||
</p>` : html``}
|
||||
${this.challenge.recoveryUrl ? html`
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
<a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
|
||||
</p>` : html``}
|
||||
</div>`;
|
||||
${this.challenge.enrollUrl
|
||||
? html` <p class="pf-c-login__main-footer-band-item">
|
||||
${t`Need an account?`}
|
||||
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
|
||||
</p>`
|
||||
: html``}
|
||||
${this.challenge.recoveryUrl
|
||||
? html` <p class="pf-c-login__main-footer-band-item">
|
||||
<a id="recovery" href="${this.challenge.recoveryUrl}"
|
||||
>${t`Forgot username or password?`}</a
|
||||
>
|
||||
</p>`
|
||||
: html``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderInput(): TemplateResult {
|
||||
let type = "text";
|
||||
if (!this.challenge?.userFields) {
|
||||
return html`<p>
|
||||
${t`Select one of the sources below to login.`}
|
||||
</p>`;
|
||||
return html`<p>${t`Select one of the sources below to login.`}</p>`;
|
||||
}
|
||||
const fields = (this.challenge?.userFields || []).sort();
|
||||
// Check if the field should be *only* email to set the input type
|
||||
|
@ -159,40 +189,48 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
[UserFieldsEnum.Email]: t`Email`,
|
||||
[UserFieldsEnum.Upn]: t`UPN`,
|
||||
};
|
||||
const label = OR_LIST_FORMATTERS.format(fields.map(f => uiFields[f]));
|
||||
const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
|
||||
return html`<ak-form-element
|
||||
label=${label}
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge.responseErrors || {})["uid_field"]}>
|
||||
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input type=${type}
|
||||
<input
|
||||
type=${type}
|
||||
name="uidField"
|
||||
placeholder=${label}
|
||||
autofocus=""
|
||||
autocomplete="username"
|
||||
class="pf-c-form-control"
|
||||
required>
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
${this.challenge.passwordFields ? html`
|
||||
<ak-form-element
|
||||
label="${t`Password`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge.responseErrors || {})["password"]}>
|
||||
<input type="password"
|
||||
name="password"
|
||||
placeholder="${t`Password`}"
|
||||
autofocus=""
|
||||
autocomplete="current-password"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
value=${PasswordManagerPrefill.password || ""}>
|
||||
</ak-form-element>
|
||||
`: html``}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
|
||||
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
|
||||
html``}
|
||||
${this.challenge.passwordFields
|
||||
? html`
|
||||
<ak-form-element
|
||||
label="${t`Password`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge.responseErrors || {})["password"]}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="${t`Password`}"
|
||||
autofocus=""
|
||||
autocomplete="current-password"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
value=${PasswordManagerPrefill.password || ""}
|
||||
/>
|
||||
</ak-form-element>
|
||||
`
|
||||
: html``}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||
? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
|
||||
: html``}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${this.challenge.primaryAction}
|
||||
|
@ -202,23 +240,21 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
${this.challenge.applicationPre ?
|
||||
html`<p>
|
||||
${t`Login to continue to ${this.challenge.applicationPre}.`}
|
||||
</p>`:
|
||||
html``}
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.challenge.applicationPre
|
||||
? html`<p>${t`Login to continue to ${this.challenge.applicationPre}.`}</p>`
|
||||
: html``}
|
||||
${this.renderInput()}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -231,5 +267,4 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
|||
${this.renderFooter()}
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,52 +17,62 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
|||
|
||||
@customElement("ak-stage-password")
|
||||
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<ak-form-static
|
||||
class="pf-c-form__group"
|
||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||
user=${this.challenge.pendingUser}>
|
||||
user=${this.challenge.pendingUser}
|
||||
>
|
||||
<div slot="link">
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
|
||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||
>${t`Not you?`}</a
|
||||
>
|
||||
</div>
|
||||
</ak-form-static>
|
||||
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pendingUser}">
|
||||
<input
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
type="hidden"
|
||||
value="${this.challenge.pendingUser}"
|
||||
/>
|
||||
<ak-form-element
|
||||
label="${t`Password`}"
|
||||
?required="${true}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}>
|
||||
<input type="password"
|
||||
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="${t`Please enter your password`}"
|
||||
autofocus=""
|
||||
autocomplete="current-password"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
value=${PasswordManagerPrefill.password || ""}>
|
||||
value=${PasswordManagerPrefill.password || ""}
|
||||
/>
|
||||
</ak-form-element>
|
||||
|
||||
${this.challenge.recoveryUrl ?
|
||||
html`<a href="${this.challenge.recoveryUrl}">
|
||||
${t`Forgot password?`}</a>` : ""}
|
||||
${this.challenge.recoveryUrl
|
||||
? html`<a href="${this.challenge.recoveryUrl}"> ${t`Forgot password?`}</a>`
|
||||
: ""}
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
|
@ -72,9 +82,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,10 +15,8 @@ import "../../../elements/EmptyState";
|
|||
import "../../../elements/Divider";
|
||||
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
|
||||
|
||||
|
||||
@customElement("ak-stage-prompt")
|
||||
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||
}
|
||||
|
@ -104,34 +102,41 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
|||
|
||||
render(): TemplateResult {
|
||||
if (!this.challenge) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
${this.challenge.flowInfo?.title}
|
||||
</h1>
|
||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.challenge.fields.map((prompt) => {
|
||||
// Special types that aren't rendered in a wrapper
|
||||
if (prompt.type === "static" || prompt.type === "hidden" || prompt.type === "separator") {
|
||||
if (
|
||||
prompt.type === "static" ||
|
||||
prompt.type === "hidden" ||
|
||||
prompt.type === "separator"
|
||||
) {
|
||||
return unsafeHTML(this.renderPromptInner(prompt));
|
||||
}
|
||||
return html`<ak-form-element
|
||||
label="${prompt.label}"
|
||||
?required="${prompt.required}"
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
|
||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||
>
|
||||
${unsafeHTML(this.renderPromptInner(prompt))}
|
||||
</ak-form-element>`;
|
||||
})}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
|
||||
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
|
||||
html``}
|
||||
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||
? this.renderNonFieldErrors(
|
||||
this.challenge?.responseErrors?.non_field_errors || [],
|
||||
)
|
||||
: html``}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${t`Continue`}
|
||||
|
@ -140,9 +145,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
|||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
</ul>
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import "../elements/messages/MessageContainer";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { me } from "../api/Users";
|
||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
|
||||
import "./locale";
|
||||
|
@ -21,24 +29,31 @@ import { AdminApi } from "authentik-api";
|
|||
import { DEFAULT_CONFIG } from "../api/Config";
|
||||
import { WebsocketClient } from "../common/ws";
|
||||
|
||||
|
||||
@customElement("ak-interface-admin")
|
||||
export class AdminInterface extends LitElement {
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
sidebarOpen = true;
|
||||
|
||||
@property({type: Boolean})
|
||||
@property({ type: Boolean })
|
||||
notificationOpen = false;
|
||||
|
||||
ws: WebsocketClient;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFPage, PFButton, PFDrawer, AKGlobal, css`
|
||||
.pf-c-page__main, .pf-c-drawer__content, .pf-c-page__drawer {
|
||||
z-index: auto !important;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFBase,
|
||||
PFPage,
|
||||
PFButton,
|
||||
PFDrawer,
|
||||
AKGlobal,
|
||||
css`
|
||||
.pf-c-page__main,
|
||||
.pf-c-drawer__content,
|
||||
.pf-c-page__drawer {
|
||||
z-index: auto !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
@ -58,56 +73,78 @@ export class AdminInterface extends LitElement {
|
|||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="pf-c-page">
|
||||
<ak-sidebar class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
|
||||
${this.renderSidebarItems()}
|
||||
</ak-sidebar>
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer ${this.notificationOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<main class="pf-c-page__main">
|
||||
<ak-router-outlet role="main" class="pf-c-page__main" tabindex="-1" id="main-content" defaultUrl="/library">
|
||||
</ak-router-outlet>
|
||||
</main>
|
||||
</div>
|
||||
return html` <div class="pf-c-page">
|
||||
<ak-sidebar
|
||||
class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}"
|
||||
>
|
||||
${this.renderSidebarItems()}
|
||||
</ak-sidebar>
|
||||
<div class="pf-c-page__drawer">
|
||||
<div
|
||||
class="pf-c-drawer ${this.notificationOpen
|
||||
? "pf-m-expanded"
|
||||
: "pf-m-collapsed"}"
|
||||
>
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<main class="pf-c-page__main">
|
||||
<ak-router-outlet
|
||||
role="main"
|
||||
class="pf-c-page__main"
|
||||
tabindex="-1"
|
||||
id="main-content"
|
||||
defaultUrl="/library"
|
||||
>
|
||||
</ak-router-outlet>
|
||||
</main>
|
||||
</div>
|
||||
<ak-notification-drawer class="pf-c-drawer__panel pf-m-width-33">
|
||||
</ak-notification-drawer>
|
||||
</div>
|
||||
<ak-notification-drawer class="pf-c-drawer__panel pf-m-width-33">
|
||||
</ak-notification-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderSidebarItems(): TemplateResult {
|
||||
const superUserCondition = () => {
|
||||
return me().then(u => u.user.isSuperuser || false);
|
||||
return me().then((u) => u.user.isSuperuser || false);
|
||||
};
|
||||
return html`
|
||||
${until(new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then(version => {
|
||||
if (version.versionCurrent !== VERSION) {
|
||||
return html`<ak-sidebar-item ?highlight=${true}>
|
||||
<span slot="label">${t`A newer version of the frontend is available.`}</span>
|
||||
</ak-sidebar-item>`;
|
||||
}
|
||||
return html``;
|
||||
}))}
|
||||
${until(me().then((u) => {
|
||||
if (u.original) {
|
||||
return html`<ak-sidebar-item ?highlight=${true} ?isAbsoluteLink=${true} path=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}>
|
||||
<span slot="label">${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span>
|
||||
</ak-sidebar-item>`;
|
||||
}
|
||||
return html``;
|
||||
}))}
|
||||
${until(
|
||||
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
|
||||
if (version.versionCurrent !== VERSION) {
|
||||
return html`<ak-sidebar-item ?highlight=${true}>
|
||||
<span slot="label"
|
||||
>${t`A newer version of the frontend is available.`}</span
|
||||
>
|
||||
</ak-sidebar-item>`;
|
||||
}
|
||||
return html``;
|
||||
}),
|
||||
)}
|
||||
${until(
|
||||
me().then((u) => {
|
||||
if (u.original) {
|
||||
return html`<ak-sidebar-item
|
||||
?highlight=${true}
|
||||
?isAbsoluteLink=${true}
|
||||
path=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}
|
||||
>
|
||||
<span slot="label"
|
||||
>${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span
|
||||
>
|
||||
</ak-sidebar-item>`;
|
||||
}
|
||||
return html``;
|
||||
}),
|
||||
)}
|
||||
<ak-sidebar-item path="/library">
|
||||
<span slot="label">${t`Library`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Monitor`}</span>
|
||||
<ak-sidebar-item path="/administration/overview">
|
||||
<span slot="label">${t`Overview`}</span>
|
||||
|
@ -116,24 +153,31 @@ export class AdminInterface extends LitElement {
|
|||
<span slot="label">${t`System Tasks`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Resources`}</span>
|
||||
<ak-sidebar-item path="/core/applications" .activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/core/applications"
|
||||
.activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Applications`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/core/sources" .activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/core/sources"
|
||||
.activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Sources`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/core/providers" .activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/core/providers"
|
||||
.activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Providers`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/core/tenants">
|
||||
<span slot="label">${t`Tenants`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Outposts`}</span>
|
||||
<ak-sidebar-item path="/outpost/outposts">
|
||||
<span slot="label">${t`Outposts`}</span>
|
||||
|
@ -142,10 +186,12 @@ export class AdminInterface extends LitElement {
|
|||
<span slot="label">${t`Service Connections`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Events`}</span>
|
||||
<ak-sidebar-item path="/events/log" .activeWhen=${[`^/events/log/(?<id>${UUID_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/events/log"
|
||||
.activeWhen=${[`^/events/log/(?<id>${UUID_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Logs`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/events/rules">
|
||||
|
@ -155,8 +201,7 @@ export class AdminInterface extends LitElement {
|
|||
<span slot="label">${t`Notification Transports`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Customisation`}</span>
|
||||
<ak-sidebar-item path="/policy/policies">
|
||||
<span slot="label">${t`Policies`}</span>
|
||||
|
@ -171,10 +216,12 @@ export class AdminInterface extends LitElement {
|
|||
<span slot="label">${t`Property Mappings`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Flows`}</span>
|
||||
<ak-sidebar-item path="/flow/flows" .activeWhen=${[`^/flow/flows/(?<slug>${SLUG_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/flow/flows"
|
||||
.activeWhen=${[`^/flow/flows/(?<slug>${SLUG_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Flows`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/flow/stages">
|
||||
|
@ -187,10 +234,12 @@ export class AdminInterface extends LitElement {
|
|||
<span slot="label">${t`Invitations`}</span>
|
||||
</ak-sidebar-item>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item
|
||||
.condition=${superUserCondition}>
|
||||
<ak-sidebar-item .condition=${superUserCondition}>
|
||||
<span slot="label">${t`Identity & Cryptography`}</span>
|
||||
<ak-sidebar-item path="/identity/users" .activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]}>
|
||||
<ak-sidebar-item
|
||||
path="/identity/users"
|
||||
.activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]}
|
||||
>
|
||||
<span slot="label">${t`Users`}</span>
|
||||
</ak-sidebar-item>
|
||||
<ak-sidebar-item path="/identity/groups">
|
||||
|
@ -205,5 +254,4 @@ export class AdminInterface extends LitElement {
|
|||
</ak-sidebar-item>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
|
||||
<script src="/static/dist/poly.js" type="module"></script>
|
||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
<script>
|
||||
window["polymerSkipLoadingFontRoboto"] = true;
|
||||
</script>
|
||||
<script src="/static/dist/AdminInterface.js" type="module"></script>
|
||||
<title>authentik</title>
|
||||
</head>
|
||||
|
@ -17,9 +19,13 @@
|
|||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state" style="height: 100vh">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
|
||||
<span
|
||||
class="pf-c-spinner pf-m-xl pf-c-empty-state__icon"
|
||||
role="progressbar"
|
||||
aria-valuetext="Loading..."
|
||||
>
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
||||
<script>ShadyDOM = { force: !navigator.webdriver }; window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/patternfly-base.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/empty-state.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
|
||||
<script>
|
||||
ShadyDOM = { force: !navigator.webdriver };
|
||||
window["polymerSkipLoadingFontRoboto"] = true;
|
||||
</script>
|
||||
<script src="/static/dist/poly.js" type="module"></script>
|
||||
<script src="/static/dist/FlowInterface.js" type="module"></script>
|
||||
<title>authentik</title>
|
||||
|
@ -17,9 +20,13 @@
|
|||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state" style="height: 100vh">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="Loading...">
|
||||
<span
|
||||
class="pf-c-spinner pf-m-xl pf-c-empty-state__icon"
|
||||
role="progressbar"
|
||||
aria-valuetext="Loading..."
|
||||
>
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
import { t } from "@lingui/macro";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import { Application, CoreApi } from "authentik-api";
|
||||
|
@ -20,11 +28,15 @@ import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
|
|||
|
||||
@customElement("ak-library-app")
|
||||
export class LibraryApplication extends LitElement {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
application?: Application;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFAvatar, AKGlobal,
|
||||
return [
|
||||
PFBase,
|
||||
PFCard,
|
||||
PFAvatar,
|
||||
AKGlobal,
|
||||
css`
|
||||
a {
|
||||
height: 100%;
|
||||
|
@ -48,7 +60,7 @@ export class LibraryApplication extends LitElement {
|
|||
justify-content: center;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
`
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -56,19 +68,28 @@ export class LibraryApplication extends LitElement {
|
|||
if (!this.application) {
|
||||
return html`<ak-spinner></ak-spinner>`;
|
||||
}
|
||||
return html` <a href="${ifDefined(this.application.launchUrl ?? "")}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
||||
return html` <a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
class="pf-c-card pf-m-hoverable pf-m-compact"
|
||||
>
|
||||
<div class="pf-c-card__header">
|
||||
${this.application.metaIcon
|
||||
? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.metaIcon)}" alt="Application Icon"/>`
|
||||
? html`<img
|
||||
class="app-icon pf-c-avatar"
|
||||
src="${ifDefined(this.application.metaIcon)}"
|
||||
alt="Application Icon"
|
||||
/>`
|
||||
: html`<i class="fas fas fa-share-square"></i>`}
|
||||
${until(me().then((u) => {
|
||||
if (!u.user.isSuperuser) return html``;
|
||||
return html`
|
||||
<a href="#/core/applications/${this.application?.slug}">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</a>
|
||||
`;
|
||||
}))}
|
||||
${until(
|
||||
me().then((u) => {
|
||||
if (!u.user.isSuperuser) return html``;
|
||||
return html`
|
||||
<a href="#/core/applications/${this.application?.slug}">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</a>
|
||||
`;
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
<div class="pf-c-card__title">
|
||||
<p id="card-1-check-label">${this.application.name}</p>
|
||||
|
@ -76,15 +97,13 @@ export class LibraryApplication extends LitElement {
|
|||
<small>${this.application.metaPublisher}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">${truncate(this.application.metaDescription, 35)}</div>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@customElement("ak-library")
|
||||
export class LibraryPage extends LitElement {
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
apps?: AKResponse<Application>;
|
||||
|
||||
pageTitle(): string {
|
||||
|
@ -120,20 +139,23 @@ export class LibraryPage extends LitElement {
|
|||
|
||||
renderApps(): TemplateResult {
|
||||
return html`<div class="pf-l-gallery pf-m-gutter">
|
||||
${this.apps?.results.map((app) => html`<ak-library-app .application=${app}></ak-library-app>`)}
|
||||
${this.apps?.results.map(
|
||||
(app) => html`<ak-library-app .application=${app}></ak-library-app>`,
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
||||
<ak-page-header
|
||||
icon="pf-icon pf-icon-applications"
|
||||
header=${t`Applications`}>
|
||||
<ak-page-header icon="pf-icon pf-icon-applications" header=${t`Applications`}>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section">
|
||||
${loading(this.apps, html`${(this.apps?.results.length || 0) > 0 ?
|
||||
this.renderApps() :
|
||||
this.renderEmptyState()}`)}
|
||||
${loading(
|
||||
this.apps,
|
||||
html`${(this.apps?.results.length || 0) > 0
|
||||
? this.renderApps()
|
||||
: this.renderEmptyState()}`,
|
||||
)}
|
||||
</section>
|
||||
</main>`;
|
||||
}
|
||||
|
|
|
@ -26,101 +26,173 @@ import "../../elements/PageHeader";
|
|||
|
||||
@customElement("ak-admin-overview")
|
||||
export class AdminOverviewPage extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFGrid, PFPage, PFContent, AKGlobal, css`
|
||||
.row-divider {
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
.graph-container {
|
||||
height: 20em;
|
||||
}
|
||||
.big-graph-container {
|
||||
height: 35em;
|
||||
}
|
||||
.card-container {
|
||||
max-height: 10em;
|
||||
}
|
||||
`];
|
||||
return [
|
||||
PFGrid,
|
||||
PFPage,
|
||||
PFContent,
|
||||
AKGlobal,
|
||||
css`
|
||||
.row-divider {
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
.graph-container {
|
||||
height: 20em;
|
||||
}
|
||||
.big-graph-container {
|
||||
height: 35em;
|
||||
}
|
||||
.card-container {
|
||||
max-height: 10em;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<ak-page-header
|
||||
icon=""
|
||||
header=${t`System Overview`}
|
||||
description=${t`General system status`}>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<!-- row 1 -->
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="pf-icon pf-icon-infrastructure" header=${t`Policies`} headerLink="#/policy/policies">
|
||||
<ak-admin-status-chart-policy></ak-admin-status-chart-policy>
|
||||
</ak-aggregate-card>
|
||||
return html` <ak-page-header
|
||||
icon=""
|
||||
header=${t`System Overview`}
|
||||
description=${t`General system status`}
|
||||
>
|
||||
</ak-page-header>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<!-- row 1 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-infrastructure"
|
||||
header=${t`Policies`}
|
||||
headerLink="#/policy/policies"
|
||||
>
|
||||
<ak-admin-status-chart-policy></ak-admin-status-chart-policy>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-server"
|
||||
header=${t`Flows`}
|
||||
headerLink="#/flow/flows"
|
||||
>
|
||||
<ak-admin-status-chart-flow></ak-admin-status-chart-flow>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="fa fa-sync-alt"
|
||||
header=${t`Outpost status`}
|
||||
headerLink="#/outpost/outposts"
|
||||
>
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="fa fa-sync-alt"
|
||||
header=${t`Users`}
|
||||
headerLink="#/identity/users"
|
||||
>
|
||||
<ak-admin-status-chart-user-count></ak-admin-status-chart-user-count>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="fa fa-sync-alt"
|
||||
header=${t`Groups`}
|
||||
headerLink="#/identity/groups"
|
||||
>
|
||||
<ak-admin-status-chart-group-count></ak-admin-status-chart-group-count>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="fa fa-sync-alt"
|
||||
header=${t`LDAP Sync status`}
|
||||
headerLink="#/core/sources"
|
||||
>
|
||||
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||
<hr />
|
||||
</div>
|
||||
<!-- row 2 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
|
||||
>
|
||||
<ak-admin-status-version
|
||||
icon="pf-icon pf-icon-bundle"
|
||||
header=${t`Version`}
|
||||
headerLink="https://github.com/goauthentik/authentik/releases"
|
||||
>
|
||||
</ak-admin-status-version>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-2-col-on-md pf-m-2-col-on-xl card-container"
|
||||
>
|
||||
<ak-admin-status-card-backup
|
||||
icon="fa fa-database"
|
||||
header=${t`Backup status`}
|
||||
headerLink="#/administration/system-tasks"
|
||||
>
|
||||
</ak-admin-status-card-backup>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container"
|
||||
>
|
||||
<ak-admin-status-card-workers
|
||||
icon="pf-icon pf-icon-server"
|
||||
header=${t`Workers`}
|
||||
>
|
||||
</ak-admin-status-card-workers>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container"
|
||||
>
|
||||
<ak-admin-status-system
|
||||
icon="pf-icon pf-icon-server"
|
||||
header=${t`System status`}
|
||||
>
|
||||
</ak-admin-status-system>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||
<hr />
|
||||
</div>
|
||||
<!-- row 3 -->
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-server"
|
||||
header=${t`Logins over the last 24 hours`}
|
||||
>
|
||||
<ak-charts-admin-login></ak-charts-admin-login>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl big-graph-container"
|
||||
>
|
||||
<ak-aggregate-card
|
||||
icon="pf-icon pf-icon-server"
|
||||
header=${t`Apps with most usage`}
|
||||
>
|
||||
<ak-top-applications-table></ak-top-applications-table>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Flows`} headerLink="#/flow/flows">
|
||||
<ak-admin-status-chart-flow></ak-admin-status-chart-flow>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Outpost status`} headerLink="#/outpost/outposts">
|
||||
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Users`} headerLink="#/identity/users">
|
||||
<ak-admin-status-chart-user-count></ak-admin-status-chart-user-count>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Groups`} headerLink="#/identity/groups">
|
||||
<ak-admin-status-chart-group-count></ak-admin-status-chart-group-count>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container">
|
||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`LDAP Sync status`} headerLink="#/core/sources">
|
||||
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||
<hr>
|
||||
</div>
|
||||
<!-- row 2 -->
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container">
|
||||
<ak-admin-status-version icon="pf-icon pf-icon-bundle" header=${t`Version`} headerLink="https://github.com/goauthentik/authentik/releases">
|
||||
</ak-admin-status-version>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-2-col-on-md pf-m-2-col-on-xl card-container">
|
||||
<ak-admin-status-card-backup icon="fa fa-database" header=${t`Backup status`} headerLink="#/administration/system-tasks">
|
||||
</ak-admin-status-card-backup>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
|
||||
<ak-admin-status-card-workers icon="pf-icon pf-icon-server" header=${t`Workers`}>
|
||||
</ak-admin-status-card-workers>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
|
||||
<ak-admin-status-system icon="pf-icon pf-icon-server" header=${t`System status`}>
|
||||
</ak-admin-status-system>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||
<hr>
|
||||
</div>
|
||||
<!-- row 3 -->
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container">
|
||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Logins over the last 24 hours`}>
|
||||
<ak-charts-admin-login></ak-charts-admin-login>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl big-graph-container">
|
||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Apps with most usage`}>
|
||||
<ak-top-applications-table></ak-top-applications-table>
|
||||
</ak-aggregate-card>
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
</section>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
|||
|
||||
@customElement("ak-top-applications-table")
|
||||
export class TopApplicationsTable extends LitElement {
|
||||
|
||||
@property({attribute: false})
|
||||
@property({ attribute: false })
|
||||
topN?: EventTopPerUser[];
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
|
@ -18,41 +17,43 @@ export class TopApplicationsTable extends LitElement {
|
|||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
new EventsApi(DEFAULT_CONFIG).eventsEventsTopPerUserList({
|
||||
action: "authorize_application",
|
||||
topN: 11,
|
||||
}).then((events) => {
|
||||
this.topN = events;
|
||||
});
|
||||
new EventsApi(DEFAULT_CONFIG)
|
||||
.eventsEventsTopPerUserList({
|
||||
action: "authorize_application",
|
||||
topN: 11,
|
||||
})
|
||||
.then((events) => {
|
||||
this.topN = events;
|
||||
});
|
||||
}
|
||||
|
||||
renderRow(event: EventTopPerUser): TemplateResult {
|
||||
return html`<tr role="row">
|
||||
<td role="cell">${event.application.name}</td>
|
||||
<td role="cell">${event.countedEvents}</td>
|
||||
<td role="cell">
|
||||
${event.application.name}
|
||||
</td>
|
||||
<td role="cell">
|
||||
${event.countedEvents}
|
||||
</td>
|
||||
<td role="cell">
|
||||
<progress value="${event.countedEvents}" max="${this.topN ? this.topN[0].countedEvents : 0}"></progress>
|
||||
<progress
|
||||
value="${event.countedEvents}"
|
||||
max="${this.topN ? this.topN[0].countedEvents : 0}"
|
||||
></progress>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<table class="pf-c-table pf-m-compact" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">${t`Application`}</th>
|
||||
<th role="columnheader" scope="col">${t`Logins`}</th>
|
||||
<th role="columnheader" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
${this.topN ? this.topN.map((e) => this.renderRow(e)) : html`<ak-spinner></ak-spinner>`}
|
||||
</tbody>
|
||||
</table>`;
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">${t`Application`}</th>
|
||||
<th role="columnheader" scope="col">${t`Logins`}</th>
|
||||
<th role="columnheader" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
${this.topN
|
||||
? this.topN.map((e) => this.renderRow(e))
|
||||
: html`<ak-spinner></ak-spinner>`}
|
||||
</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ export interface AdminStatus {
|
|||
}
|
||||
|
||||
export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||
|
||||
abstract getPrimaryValue(): Promise<T>;
|
||||
|
||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||
|
@ -30,16 +29,20 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
|||
|
||||
renderInner(): TemplateResult {
|
||||
return html`<p class="center-value">
|
||||
${until(this.getPrimaryValue().then((v) => {
|
||||
this.value = v;
|
||||
return this.getStatus(v);
|
||||
}).then((status) => {
|
||||
return html`<p>
|
||||
<i class="${status.icon}"></i> ${this.renderValue()}
|
||||
</p>
|
||||
${status.message ? html`<p class="subtext">${status.message}</p>` : html``}`;
|
||||
}), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
|
||||
${until(
|
||||
this.getPrimaryValue()
|
||||
.then((v) => {
|
||||
this.value = v;
|
||||
return this.getStatus(v);
|
||||
})
|
||||
.then((status) => {
|
||||
return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||
${status.message
|
||||
? html`<p class="subtext">${status.message}</p>`
|
||||
: html``}`;
|
||||
}),
|
||||
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
|
||||
)}
|
||||
</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,22 +7,24 @@ import { convertToTitle } from "../../../utils";
|
|||
|
||||
@customElement("ak-admin-status-card-backup")
|
||||
export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
|
||||
|
||||
getPrimaryValue(): Promise<StatusEnum> {
|
||||
return new AdminApi(DEFAULT_CONFIG).adminSystemTasksRetrieve({
|
||||
id: "backup_database"
|
||||
}).then((value) => {
|
||||
return value.status;
|
||||
}).catch(() => {
|
||||
// On error (probably 404), check the config and see if the server
|
||||
// can even backup
|
||||
return config().then(c => {
|
||||
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
|
||||
return StatusEnum.Error;
|
||||
}
|
||||
return StatusEnum.Warning;
|
||||
return new AdminApi(DEFAULT_CONFIG)
|
||||
.adminSystemTasksRetrieve({
|
||||
id: "backup_database",
|
||||
})
|
||||
.then((value) => {
|
||||
return value.status;
|
||||
})
|
||||
.catch(() => {
|
||||
// On error (probably 404), check the config and see if the server
|
||||
// can even backup
|
||||
return config().then((c) => {
|
||||
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
|
||||
return StatusEnum.Error;
|
||||
}
|
||||
return StatusEnum.Warning;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
|
@ -43,9 +45,8 @@ export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
|
|||
});
|
||||
default:
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success"
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
|
|||
|
||||
@customElement("ak-admin-status-system")
|
||||
export class SystemStatusCard extends AdminStatusCard<System> {
|
||||
|
||||
now?: Date;
|
||||
|
||||
header = "OK";
|
||||
|
@ -35,12 +34,11 @@ export class SystemStatusCard extends AdminStatusCard<System> {
|
|||
}
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
message: t`Everything is ok.`
|
||||
message: t`Everything is ok.`,
|
||||
});
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
return html`${this.header}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
|
|||
|
||||
@customElement("ak-admin-status-version")
|
||||
export class VersionStatusCard extends AdminStatusCard<Version> {
|
||||
|
||||
getPrimaryValue(): Promise<Version> {
|
||||
return new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
||||
}
|
||||
|
@ -26,12 +25,11 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
|
|||
}
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
message: t`Up-to-date!`
|
||||
message: t`Up-to-date!`,
|
||||
});
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
return html`${this.value?.versionCurrent}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
|
|||
|
||||
@customElement("ak-admin-status-card-workers")
|
||||
export class WorkersStatusCard extends AdminStatusCard<number> {
|
||||
|
||||
getPrimaryValue(): Promise<number> {
|
||||
return new AdminApi(DEFAULT_CONFIG).adminWorkersRetrieve().then((workers) => {
|
||||
return workers.count;
|
||||
|
@ -21,9 +20,8 @@ export class WorkersStatusCard extends AdminStatusCard<number> {
|
|||
});
|
||||
} else {
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success"
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ interface FlowMetrics {
|
|||
|
||||
@customElement("ak-admin-status-chart-flow")
|
||||
export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
||||
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
|
@ -32,9 +31,11 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
|||
async apiRequest(): Promise<FlowMetrics> {
|
||||
const api = new FlowsApi(DEFAULT_CONFIG);
|
||||
const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0;
|
||||
const count = (await api.flowsInstancesList({
|
||||
pageSize: 1
|
||||
})).pagination.count;
|
||||
const count = (
|
||||
await api.flowsInstancesList({
|
||||
pageSize: 1,
|
||||
})
|
||||
).pagination.count;
|
||||
this.centerText = count.toString();
|
||||
return {
|
||||
count: count - cached,
|
||||
|
@ -44,24 +45,14 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
|||
|
||||
getChartData(data: FlowMetrics): ChartData {
|
||||
return {
|
||||
labels: [
|
||||
t`Total flows`,
|
||||
t`Cached flows`,
|
||||
],
|
||||
labels: [t`Total flows`, t`Cached flows`],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: [
|
||||
"#2b9af3",
|
||||
"#3e8635",
|
||||
],
|
||||
backgroundColor: ["#2b9af3", "#3e8635"],
|
||||
spanGaps: true,
|
||||
data: [
|
||||
data.count,
|
||||
data.cached,
|
||||
],
|
||||
data: [data.count, data.cached],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ interface GroupMetrics {
|
|||
|
||||
@customElement("ak-admin-status-chart-group-count")
|
||||
export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
||||
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
|
@ -30,12 +29,16 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
|||
|
||||
async apiRequest(): Promise<GroupMetrics> {
|
||||
const api = new CoreApi(DEFAULT_CONFIG);
|
||||
const count = (await api.coreGroupsList({
|
||||
pageSize: 1
|
||||
})).pagination.count;
|
||||
const superusers = (await api.coreGroupsList({
|
||||
isSuperuser: true
|
||||
})).pagination.count;
|
||||
const count = (
|
||||
await api.coreGroupsList({
|
||||
pageSize: 1,
|
||||
})
|
||||
).pagination.count;
|
||||
const superusers = (
|
||||
await api.coreGroupsList({
|
||||
isSuperuser: true,
|
||||
})
|
||||
).pagination.count;
|
||||
this.centerText = count.toString();
|
||||
return {
|
||||
count: count - superusers,
|
||||
|
@ -45,24 +48,14 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
|||
|
||||
getChartData(data: GroupMetrics): ChartData {
|
||||
return {
|
||||
labels: [
|
||||
t`Total groups`,
|
||||
t`Superuser-groups`,
|
||||
],
|
||||
labels: [t`Total groups`, t`Superuser-groups`],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: [
|
||||
"#2b9af3",
|
||||
"#3e8635",
|
||||
],
|
||||
backgroundColor: ["#2b9af3", "#3e8635"],
|
||||
spanGaps: true,
|
||||
data: [
|
||||
data.count,
|
||||
data.superusers,
|
||||
],
|
||||
data: [data.count, data.superusers],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ interface LDAPSyncStats {
|
|||
|
||||
@customElement("ak-admin-status-chart-ldap-sync")
|
||||
export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
||||
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
|
@ -36,56 +35,45 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
|||
let healthy = 0;
|
||||
let failed = 0;
|
||||
let unsynced = 0;
|
||||
await Promise.all(sources.results.map(async (element) => {
|
||||
try {
|
||||
const health = await api.sourcesLdapSyncStatusRetrieve({
|
||||
slug: element.slug,
|
||||
});
|
||||
if (health.status !== StatusEnum.Successful) {
|
||||
failed += 1;
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || (now - health.taskFinishTimestamp.getTime()) > maxDelta) {
|
||||
await Promise.all(
|
||||
sources.results.map(async (element) => {
|
||||
try {
|
||||
const health = await api.sourcesLdapSyncStatusRetrieve({
|
||||
slug: element.slug,
|
||||
});
|
||||
if (health.status !== StatusEnum.Successful) {
|
||||
failed += 1;
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
|
||||
unsynced += 1;
|
||||
} else {
|
||||
healthy += 1;
|
||||
}
|
||||
} catch {
|
||||
unsynced += 1;
|
||||
} else {
|
||||
healthy += 1;
|
||||
}
|
||||
} catch {
|
||||
unsynced += 1;
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
this.centerText = sources.pagination.count.toString();
|
||||
return {
|
||||
healthy: sources.pagination.count === 0 ? -1 : healthy,
|
||||
failed,
|
||||
unsynced
|
||||
unsynced,
|
||||
};
|
||||
}
|
||||
|
||||
getChartData(data: LDAPSyncStats): ChartData {
|
||||
return {
|
||||
labels: [
|
||||
t`Healthy sources`,
|
||||
t`Failed sources`,
|
||||
t`Unsynced sources`,
|
||||
],
|
||||
labels: [t`Healthy sources`, t`Failed sources`, t`Unsynced sources`],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: [
|
||||
"#3e8635",
|
||||
"#C9190B",
|
||||
"#2b9af3",
|
||||
],
|
||||
backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"],
|
||||
spanGaps: true,
|
||||
data: [
|
||||
data.healthy,
|
||||
data.failed,
|
||||
data.unsynced
|
||||
],
|
||||
data: [data.healthy, data.failed, data.unsynced],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ interface OutpostStats {
|
|||
|
||||
@customElement("ak-admin-status-chart-outpost")
|
||||
export class OutpostStatusChart extends AKChart<OutpostStats> {
|
||||
|
||||
getChartType(): string {
|
||||
return "doughnut";
|
||||
}
|
||||
|
@ -36,52 +35,41 @@ export class OutpostStatusChart extends AKChart<OutpostStats> {
|
|||
let healthy = 0;
|
||||
let outdated = 0;
|
||||
let unhealthy = 0;
|
||||
await Promise.all(outposts.results.map(async (element) => {
|
||||
const health = await api.outpostsInstancesHealthList({
|
||||
uuid: element.pk || "",
|
||||
});
|
||||
if (health.length === 0) {
|
||||
unhealthy += 1;
|
||||
}
|
||||
health.forEach(h => {
|
||||
if (h.versionOutdated) {
|
||||
outdated += 1;
|
||||
} else {
|
||||
healthy += 1;
|
||||
await Promise.all(
|
||||
outposts.results.map(async (element) => {
|
||||
const health = await api.outpostsInstancesHealthList({
|
||||
uuid: element.pk || "",
|
||||
});
|
||||
if (health.length === 0) {
|
||||
unhealthy += 1;
|
||||
}
|
||||
});
|
||||
}));
|
||||
health.forEach((h) => {
|
||||
if (h.versionOutdated) {
|
||||
outdated += 1;
|
||||
} else {
|
||||
healthy += 1;
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.centerText = outposts.pagination.count.toString();
|
||||
return {
|
||||
healthy: outposts.pagination.count === 0 ? -1 : healthy,
|
||||
outdated,
|
||||
unhealthy
|
||||
unhealthy,
|
||||
};
|
||||
}
|
||||
|
||||
getChartData(data: OutpostStats): ChartData {
|
||||
return {
|
||||
labels: [
|
||||
t`Healthy outposts`,
|
||||
t`Outdated outposts`,
|
||||
t`Unhealthy outposts`,
|
||||
],
|
||||
labels: [t`Healthy outposts`, t`Outdated outposts`, t`Unhealthy outposts`],
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: [
|
||||
"#3e8635",
|
||||
"#f0ab00",
|
||||
"#C9190B",
|
||||
],
|
||||
backgroundColor: ["#3e8635", "#f0ab00", "#C9190B"],
|
||||
spanGaps: true,
|
||||
data: [
|
||||
data.healthy,
|
||||
data.outdated,
|
||||
data.unhealthy
|
||||
],
|
||||
data: [data.healthy, data.outdated, data.unhealthy],
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue