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": [
|
"presets": ["@babel/env", "@babel/typescript"],
|
||||||
"@babel/env",
|
|
||||||
"@babel/typescript"
|
|
||||||
],
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
["@babel/plugin-proposal-private-methods", { "loose": true }],
|
||||||
[
|
[
|
||||||
|
|
|
@ -14,11 +14,7 @@
|
||||||
"ecmaVersion": 12,
|
"ecmaVersion": 12,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["@typescript-eslint", "lit", "custom-elements"],
|
||||||
"@typescript-eslint",
|
|
||||||
"lit",
|
|
||||||
"custom-elements"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": "off",
|
"indent": "off",
|
||||||
"linebreak-style": ["error", "unix"],
|
"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'
|
command: 'custom'
|
||||||
workingDir: 'web/'
|
workingDir: 'web/'
|
||||||
customCommand: 'run lint'
|
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
|
- job: lit_analyse
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"lit-element": "^2.5.1",
|
"lit-element": "^2.5.1",
|
||||||
"lit-html": "^1.4.1",
|
"lit-html": "^1.4.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
"rapidoc": "^9.0.0",
|
"rapidoc": "^9.0.0",
|
||||||
"rollup": "^2.55.1",
|
"rollup": "^2.55.1",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
|
@ -66,8 +67,8 @@
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"name": "authentik-api",
|
"name": "authentik-api",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
"version": "3.9.9",
|
"version": "3.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
"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,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -6405,6 +6407,17 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/pretty-format": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
||||||
|
@ -10217,7 +10230,8 @@
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "3.9.9",
|
"version": "3.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
|
"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": {
|
"pretty-format": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",
|
"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",
|
"build": "lingui compile && rollup -c ./rollup.config.js",
|
||||||
"watch": "lingui compile && rollup -c -w",
|
"watch": "lingui compile && rollup -c -w",
|
||||||
"lint": "eslint . --max-warnings 0 --fix",
|
"lint": "eslint . --max-warnings 0 --fix",
|
||||||
"lit-analyse": "lit-analyzer src"
|
"lit-analyse": "lit-analyzer src",
|
||||||
|
"prettier-check": "prettier --check .",
|
||||||
|
"prettier": "prettier --write ."
|
||||||
},
|
},
|
||||||
"lingui": {
|
"lingui": {
|
||||||
"sourceLocale": "en",
|
"sourceLocale": "en",
|
||||||
|
@ -76,6 +78,7 @@
|
||||||
"lit-element": "^2.5.1",
|
"lit-element": "^2.5.1",
|
||||||
"lit-html": "^1.4.1",
|
"lit-html": "^1.4.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
"rapidoc": "^9.0.0",
|
"rapidoc": "^9.0.0",
|
||||||
"rollup": "^2.55.1",
|
"rollup": "^2.55.1",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
|
|
|
@ -8,21 +8,37 @@ import copy from "rollup-plugin-copy";
|
||||||
import babel from "@rollup/plugin-babel";
|
import babel from "@rollup/plugin-babel";
|
||||||
import replace from "@rollup/plugin-replace";
|
import replace from "@rollup/plugin-replace";
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [".js", ".jsx", ".ts", ".tsx"];
|
||||||
".js", ".jsx", ".ts", ".tsx",
|
|
||||||
];
|
|
||||||
|
|
||||||
const resources = [
|
const resources = [
|
||||||
{ src: "node_modules/rapidoc/dist/rapidoc-min.js", dest: "dist/" },
|
{ 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/patternfly.min.css",
|
||||||
{ src: "node_modules/@patternfly/patternfly/components/Page/page.css", dest: "dist/" },
|
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-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: "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: "src/assets/*", dest: "dist/assets" },
|
||||||
{ src: "./icons/*", dest: "dist/assets/icons" },
|
{ src: "./icons/*", dest: "dist/assets/icons" },
|
||||||
];
|
];
|
||||||
|
@ -60,15 +76,15 @@ export default [
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
typescript({
|
||||||
"declaration": true,
|
declaration: true,
|
||||||
"outDir": "./api/dist/",
|
outDir: "./api/dist/",
|
||||||
}),
|
}),
|
||||||
isProdBuild && terser(),
|
isProdBuild && terser(),
|
||||||
copy({
|
copy({
|
||||||
targets: [...resources],
|
targets: [...resources],
|
||||||
copyOnce: false,
|
copyOnce: false,
|
||||||
}),
|
}),
|
||||||
].filter(p => p),
|
].filter((p) => p),
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
},
|
},
|
||||||
|
@ -81,14 +97,14 @@ export default [
|
||||||
format: "iife",
|
format: "iife",
|
||||||
file: "dist/poly.js",
|
file: "dist/poly.js",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
cssimport(),
|
cssimport(),
|
||||||
resolve({ browser: true }),
|
resolve({ browser: true }),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
isProdBuild && terser(),
|
isProdBuild && terser(),
|
||||||
].filter(p => p),
|
].filter((p) => p),
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
},
|
},
|
||||||
|
@ -102,7 +118,7 @@ export default [
|
||||||
dir: "dist",
|
dir: "dist",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
manualChunks: manualChunks,
|
manualChunks: manualChunks,
|
||||||
chunkFileNames: "admin-[name].js"
|
chunkFileNames: "admin-[name].js",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -116,11 +132,11 @@ export default [
|
||||||
}),
|
}),
|
||||||
replace({
|
replace({
|
||||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||||
preventAssignment: true
|
"preventAssignment": true,
|
||||||
}),
|
}),
|
||||||
sourcemaps(),
|
sourcemaps(),
|
||||||
isProdBuild && terser(),
|
isProdBuild && terser(),
|
||||||
].filter(p => p),
|
].filter((p) => p),
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
},
|
},
|
||||||
|
@ -134,7 +150,7 @@ export default [
|
||||||
dir: "dist",
|
dir: "dist",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
manualChunks: manualChunks,
|
manualChunks: manualChunks,
|
||||||
chunkFileNames: "flow-[name].js"
|
chunkFileNames: "flow-[name].js",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -148,11 +164,11 @@ export default [
|
||||||
}),
|
}),
|
||||||
replace({
|
replace({
|
||||||
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
|
||||||
preventAssignment: true
|
"preventAssignment": true,
|
||||||
}),
|
}),
|
||||||
sourcemaps(),
|
sourcemaps(),
|
||||||
isProdBuild && terser(),
|
isProdBuild && terser(),
|
||||||
].filter(p => p),
|
].filter((p) => p),
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -169,7 +169,9 @@ body {
|
||||||
color: var(--ak-dark-foreground) !important;
|
color: var(--ak-dark-foreground) !important;
|
||||||
}
|
}
|
||||||
.pf-c-table__expandable-row.pf-m-expanded {
|
.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 */
|
/* tabs */
|
||||||
.pf-c-tabs {
|
.pf-c-tabs {
|
||||||
|
@ -214,7 +216,8 @@ body {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
/* inputs */
|
/* inputs */
|
||||||
optgroup, option {
|
optgroup,
|
||||||
|
option {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
.pf-c-input-group {
|
.pf-c-input-group {
|
||||||
|
@ -235,7 +238,10 @@ body {
|
||||||
background-color: var(--ak-dark-background-light);
|
background-color: var(--ak-dark-background-light);
|
||||||
}
|
}
|
||||||
.pf-c-button.pf-m-control {
|
.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);
|
background-color: var(--ak-dark-background-light);
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ export interface WSMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebsocketClient {
|
export class WebsocketClient {
|
||||||
|
|
||||||
messageSocket?: WebSocket;
|
messageSocket?: WebSocket;
|
||||||
retryDelay = 200;
|
retryDelay = 200;
|
||||||
|
|
||||||
|
@ -22,7 +21,8 @@ export class WebsocketClient {
|
||||||
|
|
||||||
connect(): void {
|
connect(): void {
|
||||||
if (navigator.webdriver) return;
|
if (navigator.webdriver) return;
|
||||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${window.location.host
|
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
|
||||||
|
window.location.host
|
||||||
}/ws/client/`;
|
}/ws/client/`;
|
||||||
this.messageSocket = new WebSocket(wsUrl);
|
this.messageSocket = new WebSocket(wsUrl);
|
||||||
this.messageSocket.addEventListener("open", () => {
|
this.messageSocket.addEventListener("open", () => {
|
||||||
|
@ -34,7 +34,7 @@ export class WebsocketClient {
|
||||||
if (this.retryDelay > 3000) {
|
if (this.retryDelay > 3000) {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: t`Connection error, reconnecting...`
|
message: t`Connection error, reconnecting...`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -50,12 +50,11 @@ export class WebsocketClient {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: data as WSMessage,
|
detail: data as WSMessage,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.messageSocket.addEventListener("error", () => {
|
this.messageSocket.addEventListener("error", () => {
|
||||||
this.retryDelay = this.retryDelay * 2;
|
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 from "codemirror";
|
||||||
import "codemirror/addon/display/autorefresh";
|
import "codemirror/addon/display/autorefresh";
|
||||||
|
@ -19,7 +27,7 @@ import YAML from "yaml";
|
||||||
|
|
||||||
@customElement("ak-codemirror")
|
@customElement("ak-codemirror")
|
||||||
export class CodeMirrorTextarea extends LitElement {
|
export class CodeMirrorTextarea extends LitElement {
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -83,11 +91,17 @@ export class CodeMirrorTextarea extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [CodeMirrorStyle, CodeMirrorTheme, CodeMirrorDialogStyle, CodeMirrorShowHintStyle, css`
|
return [
|
||||||
|
CodeMirrorStyle,
|
||||||
|
CodeMirrorTheme,
|
||||||
|
CodeMirrorDialogStyle,
|
||||||
|
CodeMirrorShowHintStyle,
|
||||||
|
css`
|
||||||
.CodeMirror-wrap pre {
|
.CodeMirror-wrap pre {
|
||||||
word-break: break-word !important;
|
word-break: break-word !important;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
|
@ -102,7 +116,7 @@ export class CodeMirrorTextarea extends LitElement {
|
||||||
readOnly: this.readOnly,
|
readOnly: this.readOnly,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
value: this._value
|
value: this._value,
|
||||||
});
|
});
|
||||||
this.editor.on("blur", () => {
|
this.editor.on("blur", () => {
|
||||||
this.editor?.save();
|
this.editor?.save();
|
||||||
|
|
|
@ -4,9 +4,11 @@ import AKGlobal from "../authentik.css";
|
||||||
|
|
||||||
@customElement("ak-divider")
|
@customElement("ak-divider")
|
||||||
export class Divider extends LitElement {
|
export class Divider extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, AKGlobal, css`
|
return [
|
||||||
|
PFBase,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
.separator {
|
.separator {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -15,23 +17,23 @@ export class Divider extends LitElement {
|
||||||
|
|
||||||
.separator::before,
|
.separator::before,
|
||||||
.separator::after {
|
.separator::after {
|
||||||
content: '';
|
content: "";
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: 1px solid var(--pf-global--Color--100);
|
border-bottom: 1px solid var(--pf-global--Color--100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator:not(:empty)::before {
|
.separator:not(:empty)::before {
|
||||||
margin-right: .25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator:not(:empty)::after {
|
.separator:not(:empty)::after {
|
||||||
margin-left: .25em;
|
margin-left: 0.25em;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="separator"><slot></slot></div>`;
|
return html`<div class="separator"><slot></slot></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,13 @@ import { PFSize } from "./Spinner";
|
||||||
|
|
||||||
@customElement("ak-empty-state")
|
@customElement("ak-empty-state")
|
||||||
export class EmptyState extends LitElement {
|
export class EmptyState extends LitElement {
|
||||||
|
@property({ type: String })
|
||||||
@property({type: String})
|
|
||||||
icon = "";
|
icon = "";
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
fullHeight = false;
|
fullHeight = false;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -28,14 +27,16 @@ export class EmptyState extends LitElement {
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}">
|
return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}">
|
||||||
<div class="pf-c-empty-state__content">
|
<div class="pf-c-empty-state__content">
|
||||||
${this.loading ?
|
${this.loading
|
||||||
html`<div class="pf-c-empty-state__icon">
|
? html`<div class="pf-c-empty-state__icon">
|
||||||
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
||||||
</div>`:
|
</div>`
|
||||||
html`<i class="pf-icon fa ${this.icon || "fa-question-circle"} pf-c-empty-state__icon" aria-hidden="true"></i>`}
|
: html`<i
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
class="pf-icon fa ${this.icon ||
|
||||||
${this.header}
|
"fa-question-circle"} pf-c-empty-state__icon"
|
||||||
</h1>
|
aria-hidden="true"
|
||||||
|
></i>`}
|
||||||
|
<h1 class="pf-c-title pf-m-lg">${this.header}</h1>
|
||||||
<div class="pf-c-empty-state__body">
|
<div class="pf-c-empty-state__body">
|
||||||
<slot name="body"></slot>
|
<slot name="body"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,5 +46,4 @@ export class EmptyState extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import PFExpandableSection from "../../node_modules/@patternfly/patternfly/compo
|
||||||
|
|
||||||
@customElement("ak-expand")
|
@customElement("ak-expand")
|
||||||
export class Expand extends LitElement {
|
export class Expand extends LitElement {
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
expanded = false;
|
expanded = false;
|
||||||
|
|
||||||
|
@ -20,16 +19,22 @@ export class Expand extends LitElement {
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-expandable-section ${this.expanded ? "pf-m-expanded" : ""}">
|
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=${() => {
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pf-c-expandable-section__toggle"
|
||||||
|
aria-expanded="${this.expanded}"
|
||||||
|
@click=${() => {
|
||||||
this.expanded = !this.expanded;
|
this.expanded = !this.expanded;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span class="pf-c-expandable-section__toggle-icon">
|
<span class="pf-c-expandable-section__toggle-icon">
|
||||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
</span>
|
</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>
|
</button>
|
||||||
<slot ?hidden=${!this.expanded} class="pf-c-expandable-section__content"></slot>
|
<slot ?hidden=${!this.expanded} class="pf-c-expandable-section__content"></slot>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ export enum PFColor {
|
||||||
|
|
||||||
@customElement("ak-label")
|
@customElement("ak-label")
|
||||||
export class Label extends LitElement {
|
export class Label extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
color: PFColor = PFColor.Grey;
|
color: PFColor = PFColor.Grey;
|
||||||
|
|
||||||
|
@ -45,11 +44,14 @@ export class Label extends LitElement {
|
||||||
return html`<span class="pf-c-label ${this.color}">
|
return html`<span class="pf-c-label ${this.color}">
|
||||||
<span class="pf-c-label__content">
|
<span class="pf-c-label__content">
|
||||||
<span class="pf-c-label__icon">
|
<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>
|
</span>
|
||||||
${this.text || ""}
|
${this.text || ""}
|
||||||
</span>
|
</span>
|
||||||
</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 PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||||
import AKGlobal from "../authentik.css";
|
import AKGlobal from "../authentik.css";
|
||||||
|
@ -10,19 +18,18 @@ import { EventsApi } from "../../api/dist";
|
||||||
|
|
||||||
@customElement("ak-page-header")
|
@customElement("ak-page-header")
|
||||||
export class PageHeader extends LitElement {
|
export class PageHeader extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
iconImage = false
|
iconImage = false;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
hasNotifications = false;
|
hasNotifications = false;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
set header(value: string) {
|
set header(value: string) {
|
||||||
tenant().then(tenant => {
|
tenant().then((tenant) => {
|
||||||
if (value !== "") {
|
if (value !== "") {
|
||||||
document.title = `${value} - ${tenant.brandingTitle}`;
|
document.title = `${value} - ${tenant.brandingTitle}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,7 +49,13 @@ export class PageHeader extends LitElement {
|
||||||
_header = "";
|
_header = "";
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFButton, PFPage, PFContent, AKGlobal, css`
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFButton,
|
||||||
|
PFPage,
|
||||||
|
PFContent,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -66,9 +79,10 @@ export class PageHeader extends LitElement {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
.notification-trigger.has-notifications {
|
.notification-trigger.has-notifications {
|
||||||
color: #2B9AF3;
|
color: #2b9af3;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIcon(): TemplateResult {
|
renderIcon(): TemplateResult {
|
||||||
|
@ -82,11 +96,13 @@ export class PageHeader extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
new EventsApi(DEFAULT_CONFIG)
|
||||||
|
.eventsNotificationsList({
|
||||||
seen: false,
|
seen: false,
|
||||||
ordering: "-created",
|
ordering: "-created",
|
||||||
pageSize: 1,
|
pageSize: 1,
|
||||||
}).then(r => {
|
})
|
||||||
|
.then((r) => {
|
||||||
this.hasNotifications = r.pagination.count > 0;
|
this.hasNotifications = r.pagination.count > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -99,33 +115,32 @@ export class PageHeader extends LitElement {
|
||||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
<section class="pf-c-page__main-section pf-m-light">
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
<h1>
|
<h1>${this.renderIcon()} ${this.header}</h1>
|
||||||
${this.renderIcon()}
|
${this.description ? html`<p>${this.description}</p>` : html``}
|
||||||
${this.header}
|
|
||||||
</h1>
|
|
||||||
${this.description ?
|
|
||||||
html`<p>${this.description}</p>` : html``}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<button
|
<button
|
||||||
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications ? "has-notifications" : ""}"
|
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications
|
||||||
|
? "has-notifications"
|
||||||
|
: ""}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-bell"></i>
|
<i class="fas fa-bell"></i>
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,11 @@ export class Spinner extends LitElement {
|
||||||
return html`<span
|
return html`<span
|
||||||
class="pf-c-spinner ${this.size.toString()}"
|
class="pf-c-spinner ${this.size.toString()}"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuetext="${t`Loading...`}">
|
aria-valuetext="${t`Loading...`}"
|
||||||
|
>
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
</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 { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
|
import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";
|
||||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
@ -11,11 +19,15 @@ export class Tabs extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
currentPage?: string;
|
currentPage?: string;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
vertical = false;
|
vertical = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFGlobal, PFTabs, AKGlobal, css`
|
return [
|
||||||
|
PFGlobal,
|
||||||
|
PFTabs,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
::slotted(*) {
|
::slotted(*) {
|
||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +40,8 @@ export class Tabs extends LitElement {
|
||||||
:host([vertical]) .pf-c-tabs__list {
|
:host([vertical]) .pf-c-tabs__list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
observer: MutationObserver;
|
observer: MutationObserver;
|
||||||
|
@ -42,7 +55,11 @@ export class Tabs extends LitElement {
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.observer.observe(this, { attributes: true, childList: true, subtree: true });
|
this.observer.observe(this, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
|
@ -61,9 +78,7 @@ export class Tabs extends LitElement {
|
||||||
const slot = page.attributes.getNamedItem("slot")?.value;
|
const slot = page.attributes.getNamedItem("slot")?.value;
|
||||||
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
|
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
|
||||||
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
|
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
|
||||||
<span class="pf-c-tabs__item-text">
|
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
|
||||||
${page.getAttribute("data-tab-title")}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,11 @@ import { MessageLevel } from "../messages/Message";
|
||||||
|
|
||||||
@customElement("ak-action-button")
|
@customElement("ak-action-button")
|
||||||
export class ActionButton extends SpinnerButton {
|
export class ActionButton extends SpinnerButton {
|
||||||
|
@property({ attribute: false })
|
||||||
@property({attribute: false})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
apiRequest: () => Promise<any> = () => { throw new Error(); };
|
apiRequest: () => Promise<any> = () => {
|
||||||
|
throw new Error();
|
||||||
|
};
|
||||||
|
|
||||||
callAction = (): Promise<void> => {
|
callAction = (): Promise<void> => {
|
||||||
this.setLoading();
|
this.setLoading();
|
||||||
|
@ -16,13 +17,13 @@ export class ActionButton extends SpinnerButton {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: e.toString()
|
message: e.toString(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
e.text().then(t => {
|
e.text().then((t) => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css";
|
import PFModalBox from "@patternfly/patternfly/components/ModalBox/modal-box.css";
|
||||||
|
@ -35,11 +43,25 @@ export class ModalButton extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
size: PFSize = PFSize.Large;
|
size: PFSize = PFSize.Large;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
open = false;
|
open = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
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() {
|
constructor() {
|
||||||
|
@ -53,7 +75,7 @@ export class ModalButton extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetForms(): void {
|
resetForms(): void {
|
||||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
|
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
|
||||||
if ("resetForm" in form) {
|
if ("resetForm" in form) {
|
||||||
form?.resetForm();
|
form?.resetForm();
|
||||||
}
|
}
|
||||||
|
@ -62,7 +84,7 @@ export class ModalButton extends LitElement {
|
||||||
|
|
||||||
onClick(): void {
|
onClick(): void {
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.querySelectorAll("*").forEach(child => {
|
this.querySelectorAll("*").forEach((child) => {
|
||||||
if ("requestUpdate" in child) {
|
if ("requestUpdate" in child) {
|
||||||
(child as LitElement).requestUpdate();
|
(child as LitElement).requestUpdate();
|
||||||
}
|
}
|
||||||
|
@ -70,17 +92,13 @@ export class ModalButton extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderModalInner(): TemplateResult {
|
renderModalInner(): TemplateResult {
|
||||||
return html`<slot name='modal'></slot>`;
|
return html`<slot name="modal"></slot>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderModal(): TemplateResult {
|
renderModal(): TemplateResult {
|
||||||
return html`<div class="pf-c-backdrop">
|
return html`<div class="pf-c-backdrop">
|
||||||
<div class="pf-l-bullseye">
|
<div class="pf-l-bullseye">
|
||||||
<div
|
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
|
||||||
class="pf-c-modal-box ${this.size}"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
@click=${() => (this.open = false)}
|
@click=${() => (this.open = false)}
|
||||||
class="pf-c-button pf-m-plain"
|
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>
|
return html` <slot name="trigger" @click=${() => this.onClick()}></slot>
|
||||||
${this.open ? this.renderModal() : ""}`;
|
${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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.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")
|
@customElement("ak-spinner-button")
|
||||||
export class SpinnerButton extends LitElement {
|
export class SpinnerButton extends LitElement {
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -60,13 +68,16 @@ export class SpinnerButton extends LitElement {
|
||||||
}
|
}
|
||||||
this.setLoading();
|
this.setLoading();
|
||||||
if (this.callAction) {
|
if (this.callAction) {
|
||||||
this.callAction().then(() => {
|
this.callAction()
|
||||||
|
.then(() => {
|
||||||
this.setDone(SUCCESS_CLASS);
|
this.setDone(SUCCESS_CLASS);
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
this.setDone(ERROR_CLASS);
|
this.setDone(ERROR_CLASS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${this.isRunning
|
${this.isRunning
|
||||||
? html` <span class="pf-c-button__progress">
|
? html` <span class="pf-c-button__progress">
|
||||||
<ak-spinner size=${PFSize.Medium}></ak-spinner>
|
<ak-spinner size=${PFSize.Medium}></ak-spinner>
|
||||||
|
|
|
@ -17,9 +17,11 @@ export class TokenCopyButton extends ActionButton {
|
||||||
if (!this.identifier) {
|
if (!this.identifier) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTokensViewKeyRetrieve({
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
identifier: this.identifier
|
.coreTokensViewKeyRetrieve({
|
||||||
}).then((token) => {
|
identifier: this.identifier,
|
||||||
|
})
|
||||||
|
.then((token) => {
|
||||||
if (!token.key) {
|
if (!token.key) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
@ -29,11 +31,11 @@ export class TokenCopyButton extends ActionButton {
|
||||||
this.buttonClass = PRIMARY_CLASS;
|
this.buttonClass = PRIMARY_CLASS;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
}).catch((err: Response | undefined) => {
|
})
|
||||||
return err?.json().then(errResp => {
|
.catch((err: Response | undefined) => {
|
||||||
|
return err?.json().then((errResp) => {
|
||||||
throw new Error(errResp["detail"]);
|
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 { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
|
@ -17,7 +25,8 @@ export class AggregateCard extends LitElement {
|
||||||
headerLink?: string;
|
headerLink?: string;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFCard, PFFlex, AKGlobal].concat([css`
|
return [PFBase, PFCard, PFFlex, AKGlobal].concat([
|
||||||
|
css`
|
||||||
.pf-c-card.pf-c-card-aggregate {
|
.pf-c-card.pf-c-card-aggregate {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +41,8 @@ export class AggregateCard extends LitElement {
|
||||||
.subtext {
|
.subtext {
|
||||||
font-size: var(--pf-global--FontSize--sm);
|
font-size: var(--pf-global--FontSize--sm);
|
||||||
}
|
}
|
||||||
`]);
|
`,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInner(): TemplateResult {
|
renderInner(): TemplateResult {
|
||||||
|
@ -40,9 +50,11 @@ export class AggregateCard extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeaderLink(): TemplateResult {
|
renderHeaderLink(): TemplateResult {
|
||||||
return html`${this.headerLink ? html`<a href="${this.headerLink}">
|
return html`${this.headerLink
|
||||||
|
? html`<a href="${this.headerLink}">
|
||||||
<i class="fa fa-link"> </i>
|
<i class="fa fa-link"> </i>
|
||||||
</a>` : ""}`;
|
</a>`
|
||||||
|
: ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -53,10 +65,7 @@ export class AggregateCard extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
${this.renderHeaderLink()}
|
${this.renderHeaderLink()}
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__body center-value">
|
<div class="pf-c-card__body center-value">${this.renderInner()}</div>
|
||||||
${this.renderInner()}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,14 @@ import { PFSize } from "../Spinner";
|
||||||
|
|
||||||
@customElement("ak-aggregate-card-promise")
|
@customElement("ak-aggregate-card-promise")
|
||||||
export class AggregatePromiseCard extends AggregateCard {
|
export class AggregatePromiseCard extends AggregateCard {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
promise?: Promise<Record<string, unknown>>;
|
promise?: Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
promiseProxy(): Promise<TemplateResult> {
|
promiseProxy(): Promise<TemplateResult> {
|
||||||
if (!this.promise) {
|
if (!this.promise) {
|
||||||
return new Promise<TemplateResult>(() => html``);
|
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()}`;
|
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>`)}
|
${until(this.promiseProxy(), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
|
||||||
</p>`;
|
</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
|
||||||
@customElement("ak-charts-admin-login")
|
@customElement("ak-charts-admin-login")
|
||||||
export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
||||||
|
|
||||||
apiRequest(): Promise<LoginMetrics> {
|
apiRequest(): Promise<LoginMetrics> {
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
||||||
}
|
}
|
||||||
|
@ -18,7 +17,8 @@ export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
||||||
label: "Failed Logins",
|
label: "Failed Logins",
|
||||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.loginsFailedPer1h?.map((cord) => {
|
data:
|
||||||
|
data.loginsFailedPer1h?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
@ -29,15 +29,15 @@ export class AdminLoginsChart extends AKChart<LoginMetrics> {
|
||||||
label: "Successful Logins",
|
label: "Successful Logins",
|
||||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.loginsPer1h?.map((cord) => {
|
data:
|
||||||
|
data.loginsPer1h?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@ import { ChartData } from "chart.js";
|
||||||
|
|
||||||
@customElement("ak-charts-application-authorize")
|
@customElement("ak-charts-application-authorize")
|
||||||
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
applicationSlug!: string;
|
applicationSlug!: string;
|
||||||
|
|
||||||
apiRequest(): Promise<Coordinate[]> {
|
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 {
|
getChartData(data: Coordinate[]): ChartData {
|
||||||
|
@ -21,15 +22,15 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
||||||
label: "Authorizations",
|
label: "Authorizations",
|
||||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.map((cord) => {
|
data:
|
||||||
|
data.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ArcElement, BarElement } from "chart.js";
|
||||||
import { TimeScale, LinearScale } from "chart.js";
|
import { TimeScale, LinearScale } from "chart.js";
|
||||||
import "chartjs-adapter-moment";
|
import "chartjs-adapter-moment";
|
||||||
import { FONT_COLOUR_DARK_MODE, FONT_COLOUR_LIGHT_MODE } from "../../pages/flows/FlowDiagram";
|
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(Legend, Tooltip);
|
||||||
Chart.register(LineController, BarController, DoughnutController);
|
Chart.register(LineController, BarController, DoughnutController);
|
||||||
|
@ -14,7 +14,6 @@ Chart.register(ArcElement, BarElement);
|
||||||
Chart.register(TimeScale, LinearScale);
|
Chart.register(TimeScale, LinearScale);
|
||||||
|
|
||||||
export abstract class AKChart<T> extends LitElement {
|
export abstract class AKChart<T> extends LitElement {
|
||||||
|
|
||||||
abstract apiRequest(): Promise<T>;
|
abstract apiRequest(): Promise<T>;
|
||||||
abstract getChartData(data: T): ChartData;
|
abstract getChartData(data: T): ChartData;
|
||||||
|
|
||||||
|
@ -26,7 +25,8 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
fontColour = FONT_COLOUR_LIGHT_MODE;
|
fontColour = FONT_COLOUR_LIGHT_MODE;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [css`
|
return [
|
||||||
|
css`
|
||||||
.container {
|
.container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,8 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -99,12 +100,14 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
chart.ctx.textBaseline = "middle";
|
chart.ctx.textBaseline = "middle";
|
||||||
chart.ctx.fillStyle = this.fontColour;
|
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;
|
const textY = height / 2;
|
||||||
|
|
||||||
chart.ctx.fillText(this.centerText, textX, textY);
|
chart.ctx.fillText(this.centerText, textX, textY);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +119,12 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
type: "time",
|
type: "time",
|
||||||
display: true,
|
display: true,
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (tickValue: string | number, index: number, ticks: Tick[]): string {
|
callback: function (
|
||||||
const valueStamp = (ticks[index]);
|
tickValue: string | number,
|
||||||
|
index: number,
|
||||||
|
ticks: Tick[],
|
||||||
|
): string {
|
||||||
|
const valueStamp = ticks[index];
|
||||||
const delta = Date.now() - valueStamp.value;
|
const delta = Date.now() - valueStamp.value;
|
||||||
const ago = Math.round(delta / 1000 / 3600);
|
const ago = Math.round(delta / 1000 / 3600);
|
||||||
return `${ago} Hours ago`;
|
return `${ago} Hours ago`;
|
||||||
|
@ -129,7 +136,7 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
grid: {
|
grid: {
|
||||||
color: "rgba(0, 0, 0, 0)",
|
color: "rgba(0, 0, 0, 0)",
|
||||||
},
|
},
|
||||||
offset: true
|
offset: true,
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
type: "linear",
|
type: "linear",
|
||||||
|
@ -138,7 +145,7 @@ export abstract class AKChart<T> extends LitElement {
|
||||||
grid: {
|
grid: {
|
||||||
color: "rgba(0, 0, 0, 0)",
|
color: "rgba(0, 0, 0, 0)",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
} as ChartOptions;
|
} as ChartOptions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { ChartData } from "chart.js";
|
||||||
|
|
||||||
@customElement("ak-charts-user")
|
@customElement("ak-charts-user")
|
||||||
export class UserChart extends AKChart<UserMetrics> {
|
export class UserChart extends AKChart<UserMetrics> {
|
||||||
|
@property({ type: Number })
|
||||||
@property({type: Number})
|
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
|
||||||
apiRequest(): Promise<UserMetrics> {
|
apiRequest(): Promise<UserMetrics> {
|
||||||
|
@ -23,7 +22,8 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
label: "Failed Logins",
|
label: "Failed Logins",
|
||||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.loginsFailedPer1h?.map((cord) => {
|
data:
|
||||||
|
data.loginsFailedPer1h?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
@ -34,7 +34,8 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
label: "Successful Logins",
|
label: "Successful Logins",
|
||||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.loginsPer1h?.map((cord) => {
|
data:
|
||||||
|
data.loginsPer1h?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
@ -45,15 +46,15 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
label: "Application authorizations",
|
label: "Application authorizations",
|
||||||
backgroundColor: "rgba(43, 154, 243, .5)",
|
backgroundColor: "rgba(43, 154, 243, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: data.authorizationsPer1h?.map((cord) => {
|
data:
|
||||||
|
data.authorizationsPer1h?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,10 @@ import AKGlobal from "../../authentik.css";
|
||||||
|
|
||||||
@customElement("ak-chip")
|
@customElement("ak-chip")
|
||||||
export class Chip extends LitElement {
|
export class Chip extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
value?: number | string;
|
value?: number | string;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
removable = false;
|
removable = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -24,16 +23,23 @@ export class Chip extends LitElement {
|
||||||
<span class="pf-c-chip__text">
|
<span class="pf-c-chip__text">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
${this.removable ? html`<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
|
${this.removable
|
||||||
this.dispatchEvent(new CustomEvent("remove", {
|
? html`<button
|
||||||
|
class="pf-c-button pf-m-plain"
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("remove", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
}));
|
}),
|
||||||
}}>
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
</button>` : html``}
|
</button>`
|
||||||
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { Chip } from "./Chip";
|
||||||
|
|
||||||
@customElement("ak-chip-group")
|
@customElement("ak-chip-group")
|
||||||
export class ChipGroup extends LitElement {
|
export class ChipGroup extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFChip, PFChipGroup, PFButton, AKGlobal];
|
return [PFBase, PFChip, PFChipGroup, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -20,7 +19,7 @@ export class ChipGroup extends LitElement {
|
||||||
|
|
||||||
get value(): (string | number | undefined)[] {
|
get value(): (string | number | undefined)[] {
|
||||||
const values: (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);
|
values.push(chip.value);
|
||||||
});
|
});
|
||||||
return values;
|
return values;
|
||||||
|
@ -35,5 +34,4 @@ export class ChipGroup extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,17 +55,16 @@ export class ObjectChangelog extends Table<Event> {
|
||||||
return [
|
return [
|
||||||
html`${item.action}`,
|
html`${item.action}`,
|
||||||
html`<div>${item.user?.username}</div>
|
html`<div>${item.user?.username}</div>
|
||||||
${item.user.on_behalf_of ? html`<small>
|
${item.user.on_behalf_of
|
||||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
|
||||||
</small>` : html``}`,
|
: html``}`,
|
||||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||||
html`<span>${item.clientIp || "-"}</span>`,
|
html`<span>${item.clientIp || "-"}</span>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: Event): TemplateResult {
|
renderExpanded(item: Event): TemplateResult {
|
||||||
return html`
|
return html` <td role="cell" colspan="4">
|
||||||
<td role="cell" colspan="4">
|
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,10 +76,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||||
|
|
||||||
renderEmpty(): TemplateResult {
|
renderEmpty(): TemplateResult {
|
||||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||||
<div slot="body">
|
<div slot="body">${t`No matching events could be found.`}</div>
|
||||||
${t`No matching events could be found.`}
|
|
||||||
</div>
|
|
||||||
</ak-empty-state>`);
|
</ak-empty-state>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||||
page: page,
|
page: page,
|
||||||
ordering: this.order,
|
ordering: this.order,
|
||||||
pageSize: PAGE_SIZE / 2,
|
pageSize: PAGE_SIZE / 2,
|
||||||
username: this.targetUser
|
username: this.targetUser,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,17 +46,16 @@ export class ObjectChangelog extends Table<Event> {
|
||||||
return [
|
return [
|
||||||
html`${item.action}`,
|
html`${item.action}`,
|
||||||
html`<div>${item.user?.username}</div>
|
html`<div>${item.user?.username}</div>
|
||||||
${item.user.on_behalf_of ? html`<small>
|
${item.user.on_behalf_of
|
||||||
${t`On behalf of ${item.user.on_behalf_of.username}`}
|
? html`<small> ${t`On behalf of ${item.user.on_behalf_of.username}`} </small>`
|
||||||
</small>` : html``}`,
|
: html``}`,
|
||||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||||
html`<span>${item.clientIp || "-"}</span>`,
|
html`<span>${item.clientIp || "-"}</span>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: Event): TemplateResult {
|
renderExpanded(item: Event): TemplateResult {
|
||||||
return html`
|
return html` <td role="cell" colspan="4">
|
||||||
<td role="cell" colspan="4">
|
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,10 +67,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||||
|
|
||||||
renderEmpty(): TemplateResult {
|
renderEmpty(): TemplateResult {
|
||||||
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
|
||||||
<div slot="body">
|
<div slot="body">${t`No matching events could be found.`}</div>
|
||||||
${t`No matching events could be found.`}
|
|
||||||
</div>
|
|
||||||
</ak-empty-state>`);
|
</ak-empty-state>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { showMessage } from "../messages/MessageContainer";
|
||||||
|
|
||||||
@customElement("ak-forms-confirm")
|
@customElement("ak-forms-confirm")
|
||||||
export class ConfirmationForm extends ModalButton {
|
export class ConfirmationForm extends ModalButton {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
successMessage!: string;
|
successMessage!: string;
|
||||||
@property()
|
@property()
|
||||||
|
@ -17,20 +16,22 @@ export class ConfirmationForm extends ModalButton {
|
||||||
@property()
|
@property()
|
||||||
action!: string;
|
action!: string;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
onConfirm!: () => Promise<unknown>;
|
onConfirm!: () => Promise<unknown>;
|
||||||
|
|
||||||
confirm(): Promise<void> {
|
confirm(): Promise<void> {
|
||||||
return this.onConfirm().then(() => {
|
return this.onConfirm()
|
||||||
|
.then(() => {
|
||||||
this.onSuccess();
|
this.onSuccess();
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
this.onError(e);
|
this.onError(e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
@ -68,17 +69,18 @@ export class ConfirmationForm extends ModalButton {
|
||||||
.callAction=${() => {
|
.callAction=${() => {
|
||||||
return this.confirm();
|
return this.confirm();
|
||||||
}}
|
}}
|
||||||
class="pf-m-danger">
|
class="pf-m-danger"
|
||||||
${this.action}
|
>
|
||||||
</ak-spinner-button>
|
${this.action} </ak-spinner-button
|
||||||
|
>
|
||||||
<ak-spinner-button
|
<ak-spinner-button
|
||||||
.callAction=${async () => {
|
.callAction=${async () => {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
}}
|
}}
|
||||||
class="pf-m-secondary">
|
class="pf-m-secondary"
|
||||||
|
>
|
||||||
${t`Cancel`}
|
${t`Cancel`}
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,34 +11,35 @@ import { until } from "lit-html/directives/until";
|
||||||
|
|
||||||
@customElement("ak-forms-delete")
|
@customElement("ak-forms-delete")
|
||||||
export class DeleteForm extends ModalButton {
|
export class DeleteForm extends ModalButton {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(PFList);
|
return super.styles.concat(PFList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
obj?: Record<string, unknown>;
|
obj?: Record<string, unknown>;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
objectLabel?: string;
|
objectLabel?: string;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
usedBy?: () => Promise<UsedBy[]>;
|
usedBy?: () => Promise<UsedBy[]>;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
delete!: () => Promise<unknown>;
|
delete!: () => Promise<unknown>;
|
||||||
|
|
||||||
confirm(): Promise<void> {
|
confirm(): Promise<void> {
|
||||||
return this.delete().then(() => {
|
return this.delete()
|
||||||
|
.then(() => {
|
||||||
this.onSuccess();
|
this.onSuccess();
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
this.onError(e);
|
this.onError(e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
@ -46,7 +47,7 @@ export class DeleteForm extends ModalButton {
|
||||||
|
|
||||||
onSuccess(): void {
|
onSuccess(): void {
|
||||||
showMessage({
|
showMessage({
|
||||||
message: t`Successfully deleted ${this.objectLabel} ${ this.obj?.name }`,
|
message: t`Successfully deleted ${this.objectLabel} ${this.obj?.name}`,
|
||||||
level: MessageLevel.success,
|
level: MessageLevel.success,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -67,30 +68,26 @@ export class DeleteForm extends ModalButton {
|
||||||
}
|
}
|
||||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
<h1 class="pf-c-title pf-m-2xl">
|
<h1 class="pf-c-title pf-m-2xl">${t`Delete ${this.objectLabel}`}</h1>
|
||||||
${t`Delete ${this.objectLabel}`}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
<section class="pf-c-page__main-section pf-m-light">
|
||||||
<form class="pf-c-form pf-m-horizontal">
|
<form class="pf-c-form pf-m-horizontal">
|
||||||
<p>
|
<p>${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}</p>
|
||||||
${t`Are you sure you want to delete ${this.objectLabel} ${objName} ?`}
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
${this.usedBy ? until(this.usedBy().then(usedBy => {
|
${this.usedBy
|
||||||
|
? until(
|
||||||
|
this.usedBy().then((usedBy) => {
|
||||||
if (usedBy.length < 1) {
|
if (usedBy.length < 1) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
<form class="pf-c-form pf-m-horizontal">
|
<form class="pf-c-form pf-m-horizontal">
|
||||||
<p>
|
<p>${t`The following objects use ${objName} `}</p>
|
||||||
${t`The following objects use ${objName} `}
|
|
||||||
</p>
|
|
||||||
<ul class="pf-c-list">
|
<ul class="pf-c-list">
|
||||||
${usedBy.map(ub => {
|
${usedBy.map((ub) => {
|
||||||
let consequence = "";
|
let consequence = "";
|
||||||
switch (ub.action) {
|
switch (ub.action) {
|
||||||
case UsedByActionEnum.Cascade:
|
case UsedByActionEnum.Cascade:
|
||||||
|
@ -106,29 +103,34 @@ export class DeleteForm extends ModalButton {
|
||||||
consequence = t`reference will be set to an empty value`;
|
consequence = t`reference will be set to an empty value`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return html`<li>${t`${ub.name} (${consequence})`}</li>`;
|
return html`<li>
|
||||||
|
${t`${ub.name} (${consequence})`}
|
||||||
|
</li>`;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
})) : html``}
|
}),
|
||||||
|
)
|
||||||
|
: html``}
|
||||||
<footer class="pf-c-modal-box__footer">
|
<footer class="pf-c-modal-box__footer">
|
||||||
<ak-spinner-button
|
<ak-spinner-button
|
||||||
.callAction=${() => {
|
.callAction=${() => {
|
||||||
return this.confirm();
|
return this.confirm();
|
||||||
}}
|
}}
|
||||||
class="pf-m-danger">
|
class="pf-m-danger"
|
||||||
${t`Delete`}
|
>
|
||||||
</ak-spinner-button>
|
${t`Delete`} </ak-spinner-button
|
||||||
|
>
|
||||||
<ak-spinner-button
|
<ak-spinner-button
|
||||||
.callAction=${async () => {
|
.callAction=${async () => {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
}}
|
}}
|
||||||
class="pf-m-secondary">
|
class="pf-m-secondary"
|
||||||
|
>
|
||||||
${t`Cancel`}
|
${t`Cancel`}
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,15 @@ import "@polymer/paper-input/paper-input";
|
||||||
import "@polymer/iron-form/iron-form";
|
import "@polymer/iron-form/iron-form";
|
||||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
@ -18,31 +26,38 @@ import { ValidationError } from "authentik-api";
|
||||||
import { EVENT_REFRESH } from "../../constants";
|
import { EVENT_REFRESH } from "../../constants";
|
||||||
|
|
||||||
export class APIError extends Error {
|
export class APIError extends Error {
|
||||||
|
|
||||||
constructor(public response: ValidationError) {
|
constructor(public response: ValidationError) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-form")
|
@customElement("ak-form")
|
||||||
export class Form<T> extends LitElement {
|
export class Form<T> extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
successMessage = "";
|
successMessage = "";
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
send!: (data: T) => Promise<unknown>;
|
send!: (data: T) => Promise<unknown>;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
nonFieldErrors?: string[];
|
nonFieldErrors?: string[];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFCard, PFButton, PFForm, PFAlert, PFInputGroup, PFFormControl, AKGlobal, css`
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFCard,
|
||||||
|
PFButton,
|
||||||
|
PFForm,
|
||||||
|
PFAlert,
|
||||||
|
PFInputGroup,
|
||||||
|
PFFormControl,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
select[multiple] {
|
select[multiple] {
|
||||||
height: 15em;
|
height: 15em;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInViewport(): boolean {
|
get isInViewport(): boolean {
|
||||||
|
@ -55,7 +70,9 @@ export class Form<T> extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(): void {
|
updated(): void {
|
||||||
this.shadowRoot?.querySelectorAll<HTMLInputElement>("input[name=name]").forEach(nameInput => {
|
this.shadowRoot
|
||||||
|
?.querySelectorAll<HTMLInputElement>("input[name=name]")
|
||||||
|
.forEach((nameInput) => {
|
||||||
const form = nameInput.closest("form");
|
const form = nameInput.closest("form");
|
||||||
if (form === null) {
|
if (form === null) {
|
||||||
return;
|
return;
|
||||||
|
@ -110,7 +127,7 @@ export class Form<T> extends LitElement {
|
||||||
serializeForm(form: IronFormElement): T {
|
serializeForm(form: IronFormElement): T {
|
||||||
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
||||||
const json: { [key: string]: unknown } = {};
|
const json: { [key: string]: unknown } = {};
|
||||||
elements.forEach(element => {
|
elements.forEach((element) => {
|
||||||
const values = form._serializeElementValues(element);
|
const values = form._serializeElementValues(element);
|
||||||
if (element.hidden) {
|
if (element.hidden) {
|
||||||
return;
|
return;
|
||||||
|
@ -138,19 +155,21 @@ export class Form<T> extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = this.serializeForm(ironForm);
|
const data = this.serializeForm(ironForm);
|
||||||
return this.send(data).then((r) => {
|
return this.send(data)
|
||||||
|
.then((r) => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.success,
|
level: MessageLevel.success,
|
||||||
message: this.getSuccessMessage()
|
message: this.getSuccessMessage(),
|
||||||
});
|
});
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return r;
|
return r;
|
||||||
}).catch((ex: Response | Error) => {
|
})
|
||||||
|
.catch((ex: Response | Error) => {
|
||||||
if (ex instanceof Error) {
|
if (ex instanceof Error) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
@ -166,7 +185,8 @@ export class Form<T> extends LitElement {
|
||||||
const elementName = element.name;
|
const elementName = element.name;
|
||||||
if (!elementName) return;
|
if (!elementName) return;
|
||||||
if (camelToSnake(elementName) in errorMessage) {
|
if (camelToSnake(elementName) in errorMessage) {
|
||||||
element.errorMessage = errorMessage[camelToSnake(elementName)].join(", ");
|
element.errorMessage =
|
||||||
|
errorMessage[camelToSnake(elementName)].join(", ");
|
||||||
element.invalid = true;
|
element.invalid = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -177,7 +197,8 @@ export class Form<T> extends LitElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
}).catch((ex: Error) => {
|
})
|
||||||
|
.catch((ex: Error) => {
|
||||||
// error is local or not from rest_framework
|
// error is local or not from rest_framework
|
||||||
showMessage({
|
showMessage({
|
||||||
message: ex.toString(),
|
message: ex.toString(),
|
||||||
|
@ -197,14 +218,12 @@ export class Form<T> extends LitElement {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-form__alert">
|
return html`<div class="pf-c-form__alert">
|
||||||
${this.nonFieldErrors.map(err => {
|
${this.nonFieldErrors.map((err) => {
|
||||||
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||||
<div class="pf-c-alert__icon">
|
<div class="pf-c-alert__icon">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="pf-c-alert__title">
|
<h4 class="pf-c-alert__title">${err}</h4>
|
||||||
${err}
|
|
||||||
</h4>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
@ -212,9 +231,11 @@ export class Form<T> extends LitElement {
|
||||||
|
|
||||||
renderVisible(): TemplateResult {
|
renderVisible(): TemplateResult {
|
||||||
return html`<iron-form
|
return html`<iron-form
|
||||||
@iron-form-presubmit=${(ev: Event) => { this.submit(ev); }}>
|
@iron-form-presubmit=${(ev: Event) => {
|
||||||
${this.renderNonFieldErrors()}
|
this.submit(ev);
|
||||||
${this.renderForm()}
|
}}
|
||||||
|
>
|
||||||
|
${this.renderNonFieldErrors()} ${this.renderForm()}
|
||||||
</iron-form>`;
|
</iron-form>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,5 +245,4 @@ export class Form<T> extends LitElement {
|
||||||
}
|
}
|
||||||
return this.renderVisible();
|
return this.renderVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,19 @@ import { ErrorDetail } from "authentik-api";
|
||||||
|
|
||||||
@customElement("ak-form-element")
|
@customElement("ak-form-element")
|
||||||
export class FormElement extends LitElement {
|
export class FormElement extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFForm, PFFormControl, css`
|
return [
|
||||||
|
PFForm,
|
||||||
|
PFFormControl,
|
||||||
|
css`
|
||||||
slot {
|
slot {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -28,7 +31,7 @@ export class FormElement extends LitElement {
|
||||||
errors?: ErrorDetail[];
|
errors?: ErrorDetail[];
|
||||||
|
|
||||||
updated(): void {
|
updated(): void {
|
||||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
|
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||||
input.focus();
|
input.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -37,7 +40,9 @@ export class FormElement extends LitElement {
|
||||||
return html`<div class="pf-c-form__group">
|
return html`<div class="pf-c-form__group">
|
||||||
<label class="pf-c-form__label">
|
<label class="pf-c-form__label">
|
||||||
<span class="pf-c-form__label-text">${this.label}</span>
|
<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>
|
</label>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
${(this.errors || []).map((error) => {
|
${(this.errors || []).map((error) => {
|
||||||
|
@ -45,5 +50,4 @@ export class FormElement extends LitElement {
|
||||||
})}
|
})}
|
||||||
</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 PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.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")
|
@customElement("ak-form-group")
|
||||||
export class FormGroup extends LitElement {
|
export class FormGroup extends LitElement {
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
expanded = false;
|
expanded = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFButton, PFFormControl, AKGlobal, css`
|
return [
|
||||||
slot[name=body][hidden] {
|
PFBase,
|
||||||
|
PFForm,
|
||||||
|
PFButton,
|
||||||
|
PFFormControl,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
|
slot[name="body"][hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
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">
|
||||||
<div class="pf-c-form__field-group-toggle-button">
|
<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=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-plain"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="${this.expanded}"
|
||||||
|
aria-label="Details"
|
||||||
|
@click=${() => {
|
||||||
this.expanded = !this.expanded;
|
this.expanded = !this.expanded;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span class="pf-c-form__field-group-toggle-icon">
|
<span class="pf-c-form__field-group-toggle-icon">
|
||||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
|
@ -47,5 +67,4 @@ export class FormGroup extends LitElement {
|
||||||
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
|
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,24 @@ import { t } from "@lingui/macro";
|
||||||
|
|
||||||
@customElement("ak-form-element-horizontal")
|
@customElement("ak-form-element-horizontal")
|
||||||
export class HorizontalFormElement extends LitElement {
|
export class HorizontalFormElement extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFForm, PFFormControl, AKGlobal, css`
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFForm,
|
||||||
|
PFFormControl,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
.pf-c-form__group {
|
.pf-c-form__group {
|
||||||
display: grid;
|
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);
|
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 {
|
.pf-c-form__group-label {
|
||||||
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -43,7 +50,7 @@ export class HorizontalFormElement extends LitElement {
|
||||||
name = "";
|
name = "";
|
||||||
|
|
||||||
updated(): void {
|
updated(): void {
|
||||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach(input => {
|
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||||
input.focus();
|
input.focus();
|
||||||
});
|
});
|
||||||
this.querySelectorAll("*").forEach((input) => {
|
this.querySelectorAll("*").forEach((input) => {
|
||||||
|
@ -59,7 +66,7 @@ export class HorizontalFormElement extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.writeOnly && !this.writeOnlyActivated) {
|
if (this.writeOnly && !this.writeOnlyActivated) {
|
||||||
const i = (input as HTMLInputElement);
|
const i = input as HTMLInputElement;
|
||||||
i.setAttribute("hidden", "true");
|
i.setAttribute("hidden", "true");
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
i.removeAttribute("hidden");
|
i.removeAttribute("hidden");
|
||||||
|
@ -76,24 +83,36 @@ export class HorizontalFormElement extends LitElement {
|
||||||
<div class="pf-c-form__group-label">
|
<div class="pf-c-form__group-label">
|
||||||
<label class="pf-c-form__label">
|
<label class="pf-c-form__label">
|
||||||
<span class="pf-c-form__label-text">${this.label}</span>
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-form__group-control">
|
<div class="pf-c-form__group-control">
|
||||||
${this.writeOnly && !this.writeOnlyActivated ?
|
${this.writeOnly && !this.writeOnlyActivated
|
||||||
html`<div class="pf-c-form__horizontal-group">
|
? html`<div class="pf-c-form__horizontal-group">
|
||||||
<input class="pf-c-form-control" type="password" disabled value="**************">
|
<input
|
||||||
</div>` :
|
class="pf-c-form-control"
|
||||||
html``}
|
type="password"
|
||||||
|
disabled
|
||||||
|
value="**************"
|
||||||
|
/>
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
<slot class="pf-c-form__horizontal-group"></slot>
|
<slot class="pf-c-form__horizontal-group"></slot>
|
||||||
<div class="pf-c-form__horizontal-group">
|
<div class="pf-c-form__horizontal-group">
|
||||||
${this.writeOnly ? html`<p class="pf-c-form__helper-text" aria-live="polite">${
|
${this.writeOnly
|
||||||
t`Click to change value`
|
? html`<p class="pf-c-form__helper-text" aria-live="polite">
|
||||||
}</p>` : html``}
|
${t`Click to change value`}
|
||||||
${this.invalid ? html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">${this.errorMessage}</p>` : html``}
|
</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>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import "../buttons/SpinnerButton";
|
||||||
|
|
||||||
@customElement("ak-forms-modal")
|
@customElement("ak-forms-modal")
|
||||||
export class ModalForm extends ModalButton {
|
export class ModalForm extends ModalButton {
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
closeAfterSuccessfulSubmit = true;
|
closeAfterSuccessfulSubmit = true;
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ export class ModalForm extends ModalButton {
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -50,18 +49,19 @@ export class ModalForm extends ModalButton {
|
||||||
.callAction=${() => {
|
.callAction=${() => {
|
||||||
return this.confirm();
|
return this.confirm();
|
||||||
}}
|
}}
|
||||||
class="pf-m-primary">
|
class="pf-m-primary"
|
||||||
<slot name="submit"></slot>
|
>
|
||||||
</ak-spinner-button>
|
<slot name="submit"></slot> </ak-spinner-button
|
||||||
|
>
|
||||||
<ak-spinner-button
|
<ak-spinner-button
|
||||||
.callAction=${async () => {
|
.callAction=${async () => {
|
||||||
this.resetForms();
|
this.resetForms();
|
||||||
this.open = false;
|
this.open = false;
|
||||||
}}
|
}}
|
||||||
class="pf-m-secondary">
|
class="pf-m-secondary"
|
||||||
|
>
|
||||||
${t`Cancel`}
|
${t`Cancel`}
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,13 @@ import { EVENT_REFRESH } from "../../constants";
|
||||||
import { Form } from "./Form";
|
import { Form } from "./Form";
|
||||||
|
|
||||||
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
||||||
|
|
||||||
abstract loadInstance(pk: PKT): Promise<T>;
|
abstract loadInstance(pk: PKT): Promise<T>;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
set instancePk(value: PKT) {
|
set instancePk(value: PKT) {
|
||||||
this._instancePk = value;
|
this._instancePk = value;
|
||||||
if (this.isInViewport) {
|
if (this.isInViewport) {
|
||||||
this.loadInstance(value).then(instance => {
|
this.loadInstance(value).then((instance) => {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
});
|
});
|
||||||
|
@ -32,7 +31,7 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
||||||
super();
|
super();
|
||||||
this.addEventListener(EVENT_REFRESH, () => {
|
this.addEventListener(EVENT_REFRESH, () => {
|
||||||
if (!this._instancePk) return;
|
if (!this._instancePk) return;
|
||||||
this.loadInstance(this._instancePk).then(instance => {
|
this.loadInstance(this._instancePk).then((instance) => {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -51,5 +50,4 @@ export abstract class ModelForm<T, PKT extends string | number> extends Form<T>
|
||||||
}
|
}
|
||||||
return super.render();
|
return super.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,13 @@ import { Form } from "./Form";
|
||||||
|
|
||||||
@customElement("ak-proxy-form")
|
@customElement("ak-proxy-form")
|
||||||
export class ProxyForm extends Form<unknown> {
|
export class ProxyForm extends Form<unknown> {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
type!: string;
|
type!: string;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
args: Record<string, unknown> = {};
|
args: Record<string, unknown> = {};
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
typeMap: Record<string, string> = {};
|
typeMap: Record<string, string> = {};
|
||||||
|
|
||||||
submit(ev: Event): Promise<unknown> | undefined {
|
submit(ev: Event): Promise<unknown> | undefined {
|
||||||
|
@ -43,5 +42,4 @@ export class ProxyForm extends Form<unknown> {
|
||||||
}
|
}
|
||||||
return html`${el}`;
|
return html`${el}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export enum MessageLevel {
|
||||||
error = "error",
|
error = "error",
|
||||||
warning = "warning",
|
warning = "warning",
|
||||||
success = "success",
|
success = "success",
|
||||||
info = "info"
|
info = "info",
|
||||||
}
|
}
|
||||||
export interface APIMessage {
|
export interface APIMessage {
|
||||||
level: MessageLevel;
|
level: MessageLevel;
|
||||||
|
@ -27,14 +27,13 @@ const LEVEL_ICON_MAP: { [key: string]: string } = {
|
||||||
|
|
||||||
@customElement("ak-message")
|
@customElement("ak-message")
|
||||||
export class Message extends LitElement {
|
export class Message extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
@property({attribute: false})
|
|
||||||
message?: APIMessage;
|
message?: APIMessage;
|
||||||
|
|
||||||
@property({type: Number})
|
@property({ type: Number })
|
||||||
removeAfter = 8000;
|
removeAfter = 8000;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
onRemove?: (m: APIMessage) => void;
|
onRemove?: (m: APIMessage) => void;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -51,27 +50,34 @@ export class Message extends LitElement {
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<li class="pf-c-alert-group__item">
|
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">
|
<div class="pf-c-alert__icon">
|
||||||
<i class="${this.message ? LEVEL_ICON_MAP[this.message.level] : ""}"></i>
|
<i class="${this.message ? LEVEL_ICON_MAP[this.message.level] : ""}"></i>
|
||||||
</div>
|
</div>
|
||||||
<p class="pf-c-alert__title">
|
<p class="pf-c-alert__title">${this.message?.message}</p>
|
||||||
${this.message?.message}
|
${this.message?.description &&
|
||||||
</p>
|
html`<div class="pf-c-alert__description">
|
||||||
${this.message?.description && html`<div class="pf-c-alert__description">
|
|
||||||
<p>${this.message.description}</p>
|
<p>${this.message.description}</p>
|
||||||
</div>`}
|
</div>`}
|
||||||
<div class="pf-c-alert__action">
|
<div class="pf-c-alert__action">
|
||||||
<button class="pf-c-button pf-m-plain" type="button" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-plain"
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
if (!this.message) return;
|
if (!this.message) return;
|
||||||
if (!this.onRemove) return;
|
if (!this.onRemove) return;
|
||||||
this.onRemove(this.message);
|
this.onRemove(this.message);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>`;
|
</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 "./Message";
|
||||||
import { APIMessage } from "./Message";
|
import { APIMessage } from "./Message";
|
||||||
import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css";
|
import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css";
|
||||||
|
@ -17,17 +25,20 @@ export function showMessage(message: APIMessage): void {
|
||||||
|
|
||||||
@customElement("ak-message-container")
|
@customElement("ak-message-container")
|
||||||
export class MessageContainer extends LitElement {
|
export class MessageContainer extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
@property({attribute: false})
|
|
||||||
messages: APIMessage[] = [];
|
messages: APIMessage[] = [];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFAlertGroup, css`
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFAlertGroup,
|
||||||
|
css`
|
||||||
/* Fix spacing between messages */
|
/* Fix spacing between messages */
|
||||||
ak-message {
|
ak-message {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -40,7 +51,7 @@ export class MessageContainer extends LitElement {
|
||||||
|
|
||||||
// add a new message, but only if the message isn't currently shown.
|
// add a new message, but only if the message isn't currently shown.
|
||||||
addMessage(message: APIMessage): void {
|
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) {
|
if (matchingMessages.length < 1) {
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
}
|
}
|
||||||
|
@ -54,7 +65,8 @@ export class MessageContainer extends LitElement {
|
||||||
.onRemove=${(m: APIMessage) => {
|
.onRemove=${(m: APIMessage) => {
|
||||||
this.messages = this.messages.filter((v) => v !== m);
|
this.messages = this.messages.filter((v) => v !== m);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
</ak-message>`;
|
</ak-message>`;
|
||||||
})}
|
})}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
|
|
|
@ -4,13 +4,12 @@ import { MessageLevel } from "./Message";
|
||||||
import { showMessage } from "./MessageContainer";
|
import { showMessage } from "./MessageContainer";
|
||||||
|
|
||||||
export class MessageMiddleware implements Middleware {
|
export class MessageMiddleware implements Middleware {
|
||||||
|
|
||||||
post(context: ResponseContext): Promise<Response | void> {
|
post(context: ResponseContext): Promise<Response | void> {
|
||||||
if (context.response.status >= 500) {
|
if (context.response.status >= 500) {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: t`API request failed`,
|
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);
|
return Promise.resolve(context.response);
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class APIMiddleware implements Middleware {
|
||||||
new CustomEvent(EVENT_API_DRAWER_REFRESH, {
|
new CustomEvent(EVENT_API_DRAWER_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return Promise.resolve(context.response);
|
return Promise.resolve(context.response);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,6 @@ export const API_DRAWER_MIDDLEWARE = new APIMiddleware();
|
||||||
|
|
||||||
@customElement("ak-api-drawer")
|
@customElement("ak-api-drawer")
|
||||||
export class APIDrawer extends LitElement {
|
export class APIDrawer extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal];
|
return [PFBase, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -58,9 +57,7 @@ export class APIDrawer extends LitElement {
|
||||||
renderItem(item: RequestInfo): TemplateResult {
|
renderItem(item: RequestInfo): TemplateResult {
|
||||||
return html`<li class="pf-c-notification-drawer__list-item pf-m-read">
|
return html`<li class="pf-c-notification-drawer__list-item pf-m-read">
|
||||||
<div class="pf-c-notification-drawer__list-item-header">
|
<div class="pf-c-notification-drawer__list-item-header">
|
||||||
<h2 class="pf-c-notification-drawer__list-item-header-title">
|
<h2 class="pf-c-notification-drawer__list-item-header-title">${item.method}</h2>
|
||||||
${item.method}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="pf-c-notification-drawer__list-item-description">${item.path}</p>
|
<p class="pf-c-notification-drawer__list-item-description">${item.path}</p>
|
||||||
</li>`;
|
</li>`;
|
||||||
|
@ -70,17 +67,14 @@ export class APIDrawer extends LitElement {
|
||||||
return html`<div class="pf-c-drawer__body pf-m-no-padding">
|
return html`<div class="pf-c-drawer__body pf-m-no-padding">
|
||||||
<div class="pf-c-notification-drawer">
|
<div class="pf-c-notification-drawer">
|
||||||
<div class="pf-c-notification-drawer__header pf-c-content">
|
<div class="pf-c-notification-drawer__header pf-c-content">
|
||||||
<h1>
|
<h1>${t`API Requests`}</h1>
|
||||||
${t`API Requests`}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-notification-drawer__body">
|
<div class="pf-c-notification-drawer__body">
|
||||||
<ul class="pf-c-notification-drawer__list">
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import { t } from "@lingui/macro";
|
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 { EventsApi, Notification } from "authentik-api";
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../api/Client";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
@ -14,11 +22,10 @@ import { ActionToLabel } from "../../pages/events/utils";
|
||||||
|
|
||||||
@customElement("ak-notification-drawer")
|
@customElement("ak-notification-drawer")
|
||||||
export class NotificationDrawer extends LitElement {
|
export class NotificationDrawer extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
@property({attribute: false})
|
|
||||||
notifications?: AKResponse<Notification>;
|
notifications?: AKResponse<Notification>;
|
||||||
|
|
||||||
@property({type: Number})
|
@property({ type: Number })
|
||||||
unread = 0;
|
unread = 0;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -36,15 +43,17 @@ export class NotificationDrawer extends LitElement {
|
||||||
.pf-c-notification-drawer__list-item-description {
|
.pf-c-notification-drawer__list-item-description {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
new EventsApi(DEFAULT_CONFIG)
|
||||||
|
.eventsNotificationsList({
|
||||||
seen: false,
|
seen: false,
|
||||||
ordering: "-created",
|
ordering: "-created",
|
||||||
}).then(r => {
|
})
|
||||||
|
.then((r) => {
|
||||||
this.notifications = r;
|
this.notifications = r;
|
||||||
this.unread = r.results.length;
|
this.unread = r.results.length;
|
||||||
});
|
});
|
||||||
|
@ -75,26 +84,38 @@ export class NotificationDrawer extends LitElement {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-notification-drawer__list-item-action">
|
<div class="pf-c-notification-drawer__list-item-action">
|
||||||
${item.event && html`
|
${item.event &&
|
||||||
<a class="pf-c-dropdown__toggle pf-m-plain" href="#/events/log/${item.event?.pk}">
|
html`
|
||||||
|
<a
|
||||||
|
class="pf-c-dropdown__toggle pf-m-plain"
|
||||||
|
href="#/events/log/${item.event?.pk}"
|
||||||
|
>
|
||||||
<i class="fas fas fa-share-square"></i>
|
<i class="fas fas fa-share-square"></i>
|
||||||
</a>
|
</a>
|
||||||
`}
|
`}
|
||||||
<button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => {
|
<button
|
||||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsPartialUpdate({
|
class="pf-c-dropdown__toggle pf-m-plain"
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
|
new EventsApi(DEFAULT_CONFIG)
|
||||||
|
.eventsNotificationsPartialUpdate({
|
||||||
uuid: item.pk || "",
|
uuid: item.pk || "",
|
||||||
patchedNotificationRequest: {
|
patchedNotificationRequest: {
|
||||||
seen: true,
|
seen: true,
|
||||||
}
|
},
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
this.firstUpdated();
|
this.firstUpdated();
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
|
<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>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,12 +127,8 @@ export class NotificationDrawer extends LitElement {
|
||||||
<div class="pf-c-notification-drawer">
|
<div class="pf-c-notification-drawer">
|
||||||
<div class="pf-c-notification-drawer__header">
|
<div class="pf-c-notification-drawer__header">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<h1 class="pf-c-notification-drawer__header-title">
|
<h1 class="pf-c-notification-drawer__header-title">${t`Notifications`}</h1>
|
||||||
${t`Notifications`}
|
<span> ${t`${this.unread} unread`} </span>
|
||||||
</h1>
|
|
||||||
<span>
|
|
||||||
${t`${this.unread} unread`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-notification-drawer__header-action">
|
<div class="pf-c-notification-drawer__header-action">
|
||||||
<div class="pf-c-notification-drawer__header-action-close">
|
<div class="pf-c-notification-drawer__header-action-close">
|
||||||
|
@ -121,12 +138,13 @@ export class NotificationDrawer extends LitElement {
|
||||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
class="pf-c-button pf-m-plain"
|
class="pf-c-button pf-m-plain"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close">
|
aria-label="Close"
|
||||||
|
>
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,11 +152,10 @@ export class NotificationDrawer extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-notification-drawer__body">
|
<div class="pf-c-notification-drawer__body">
|
||||||
<ul class="pf-c-notification-drawer__list">
|
<ul class="pf-c-notification-drawer__list">
|
||||||
${this.notifications.results.map(n => this.renderItem(n))}
|
${this.notifications.results.map((n) => this.renderItem(n))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,30 +35,27 @@ export class UserOAuthCodeList extends Table<ExpiringBaseGrantModel> {
|
||||||
|
|
||||||
row(item: ExpiringBaseGrantModel): TemplateResult[] {
|
row(item: ExpiringBaseGrantModel): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||||
${item.provider?.name}
|
|
||||||
</a>`,
|
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires?.toLocaleString()}`,
|
||||||
html`${item.scope.join(", ")}`,
|
html`${item.scope.join(", ")}`,
|
||||||
html`
|
html` <ak-forms-delete
|
||||||
<ak-forms-delete
|
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${t`Authorization Code`}
|
objectLabel=${t`Authorization Code`}
|
||||||
.usedBy=${() => {
|
.usedBy=${() => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesUsedByList({
|
||||||
id: item.pk
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.delete=${() => {
|
.delete=${() => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2AuthorizationCodesDestroy({
|
||||||
id: item.pk,
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
${t`Delete Authorization Code`}
|
${t`Delete Authorization Code`}
|
||||||
</button>
|
</button>
|
||||||
</ak-forms-delete>`,
|
</ak-forms-delete>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,7 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExpanded(item: RefreshTokenModel): TemplateResult {
|
renderExpanded(item: RefreshTokenModel): TemplateResult {
|
||||||
return html`
|
return html` <td role="cell" colspan="4">
|
||||||
<td role="cell" colspan="4">
|
|
||||||
<div class="pf-c-table__expandable-row-content">
|
<div class="pf-c-table__expandable-row-content">
|
||||||
<div class="pf-l-flex">
|
<div class="pf-l-flex">
|
||||||
<div class="pf-l-flex__item">
|
<div class="pf-l-flex__item">
|
||||||
|
@ -60,31 +59,28 @@ export class UserOAuthRefreshList extends Table<RefreshTokenModel> {
|
||||||
|
|
||||||
row(item: RefreshTokenModel): TemplateResult[] {
|
row(item: RefreshTokenModel): TemplateResult[] {
|
||||||
return [
|
return [
|
||||||
html`<a href="#/core/providers/${item.provider?.pk}">
|
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
|
||||||
${item.provider?.name}
|
|
||||||
</a>`,
|
|
||||||
html`${item.revoked ? t`Yes` : t`No`}`,
|
html`${item.revoked ? t`Yes` : t`No`}`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires?.toLocaleString()}`,
|
||||||
html`${item.scope.join(", ")}`,
|
html`${item.scope.join(", ")}`,
|
||||||
html`
|
html` <ak-forms-delete
|
||||||
<ak-forms-delete
|
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${t`Refresh Code`}
|
objectLabel=${t`Refresh Code`}
|
||||||
.usedBy=${() => {
|
.usedBy=${() => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
|
||||||
id: item.pk
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.delete=${() => {
|
.delete=${() => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
|
||||||
id: item.pk,
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
${t`Delete Refresh Code`}
|
${t`Delete Refresh Code`}
|
||||||
</button>
|
</button>
|
||||||
</ak-forms-delete>`,
|
</ak-forms-delete>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Route } from "./Route";
|
||||||
|
|
||||||
export class RouteMatch {
|
export class RouteMatch {
|
||||||
route: Route;
|
route: Route;
|
||||||
arguments: { [key: string]: string; };
|
arguments: { [key: string]: string };
|
||||||
fullUrl?: string;
|
fullUrl?: string;
|
||||||
|
|
||||||
constructor(route: Route) {
|
constructor(route: Route) {
|
||||||
|
@ -16,6 +16,8 @@ export class RouteMatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
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")
|
@customElement("ak-router-404")
|
||||||
export class Router404 extends LitElement {
|
export class Router404 extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
url = "";
|
url = "";
|
||||||
|
|
||||||
|
@ -19,9 +18,7 @@ export class Router404 extends LitElement {
|
||||||
<div class="pf-c-empty-state__content">
|
<div class="pf-c-empty-state__content">
|
||||||
<i class="fas fa-question-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
<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>
|
<h1 class="pf-c-title pf-m-lg">${t`Not found`}</h1>
|
||||||
<div class="pf-c-empty-state__body">
|
<div class="pf-c-empty-state__body">${t`The URL "${this.url}" was not found.`}</div>
|
||||||
${t`The URL "${this.url}" was not found.`}
|
|
||||||
</div>
|
|
||||||
<a href="#/" class="pf-c-button pf-m-primary" type="button">${t`Return home`}</a>
|
<a href="#/" class="pf-c-button pf-m-primary" type="button">${t`Return home`}</a>
|
||||||
</div>
|
</div>
|
||||||
</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 { Route } from "./Route";
|
||||||
import { ROUTES } from "../../routes";
|
import { ROUTES } from "../../routes";
|
||||||
import { RouteMatch } from "./RouteMatch";
|
import { RouteMatch } from "./RouteMatch";
|
||||||
|
@ -10,26 +18,36 @@ import { ROUTE_SEPARATOR } from "../../constants";
|
||||||
// Poliyfill for hashchange.newURL,
|
// Poliyfill for hashchange.newURL,
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
|
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
if (!window.HashChangeEvent) (function () {
|
if (!window.HashChangeEvent)
|
||||||
|
(function () {
|
||||||
let lastURL = document.URL;
|
let lastURL = document.URL;
|
||||||
window.addEventListener("hashchange", function (event) {
|
window.addEventListener("hashchange", function (event) {
|
||||||
Object.defineProperty(event, "oldURL", { enumerable: true, configurable: true, value: lastURL });
|
Object.defineProperty(event, "oldURL", {
|
||||||
Object.defineProperty(event, "newURL", { enumerable: true, configurable: true, value: document.URL });
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: lastURL,
|
||||||
|
});
|
||||||
|
Object.defineProperty(event, "newURL", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: document.URL,
|
||||||
|
});
|
||||||
lastURL = document.URL;
|
lastURL = document.URL;
|
||||||
});
|
});
|
||||||
}());
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
@customElement("ak-router-outlet")
|
@customElement("ak-router-outlet")
|
||||||
export class RouterOutlet extends LitElement {
|
export class RouterOutlet extends LitElement {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
current?: RouteMatch;
|
current?: RouteMatch;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
defaultUrl?: string;
|
defaultUrl?: string;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [AKGlobal,
|
return [
|
||||||
|
AKGlobal,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
@ -88,7 +106,7 @@ export class RouterOutlet extends LitElement {
|
||||||
RegExp(""),
|
RegExp(""),
|
||||||
html`<div class="pf-c-page__main">
|
html`<div class="pf-c-page__main">
|
||||||
<ak-router-404 url=${activeUrl}></ak-router-404>
|
<ak-router-404 url=${activeUrl}></ak-router-404>
|
||||||
</div>`
|
</div>`,
|
||||||
);
|
);
|
||||||
matchedRoute = new RouteMatch(route);
|
matchedRoute = new RouteMatch(route);
|
||||||
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
|
matchedRoute.arguments = route.url.exec(activeUrl)?.groups || {};
|
||||||
|
|
|
@ -9,7 +9,6 @@ import "./SidebarUser";
|
||||||
|
|
||||||
@customElement("ak-sidebar")
|
@customElement("ak-sidebar")
|
||||||
export class Sidebar extends LitElement {
|
export class Sidebar extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
PFBase,
|
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 PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
@ -25,7 +33,7 @@ export const DefaultTenant: CurrentTenant = {
|
||||||
|
|
||||||
@customElement("ak-sidebar-brand")
|
@customElement("ak-sidebar-brand")
|
||||||
export class SidebarBrand extends LitElement {
|
export class SidebarBrand extends LitElement {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
tenant: CurrentTenant = DefaultTenant;
|
tenant: CurrentTenant = DefaultTenant;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -45,7 +53,7 @@ export class SidebarBrand extends LitElement {
|
||||||
}
|
}
|
||||||
.pf-c-brand img {
|
.pf-c-brand img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 .5rem;
|
padding: 0 0.5rem;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
}
|
}
|
||||||
button.pf-c-button.sidebar-trigger {
|
button.pf-c-button.sidebar-trigger {
|
||||||
|
@ -67,12 +75,12 @@ export class SidebarBrand extends LitElement {
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
configureSentry(true);
|
configureSentry(true);
|
||||||
tenant().then(tenant => this.tenant = tenant);
|
tenant().then((tenant) => (this.tenant = tenant));
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html` ${window.innerWidth <= MIN_WIDTH
|
||||||
${window.innerWidth <= MIN_WIDTH ? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
class="sidebar-trigger pf-c-button"
|
class="sidebar-trigger pf-c-button"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
|
@ -80,15 +88,21 @@ export class SidebarBrand extends LitElement {
|
||||||
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
` : html``}
|
`
|
||||||
|
: html``}
|
||||||
<a href="#/" class="pf-c-page__header-brand-link">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<div class="pf-c-brand ak-brand">
|
<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>
|
</div>
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { ROUTE_SEPARATOR } from "../../constants";
|
||||||
|
|
||||||
@customElement("ak-sidebar-item")
|
@customElement("ak-sidebar-item")
|
||||||
export class SidebarItem extends LitElement {
|
export class SidebarItem extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
PFBase,
|
PFBase,
|
||||||
|
@ -92,13 +91,13 @@ export class SidebarItem extends LitElement {
|
||||||
|
|
||||||
get childItems(): SidebarItem[] {
|
get childItems(): SidebarItem[] {
|
||||||
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
|
const children = Array.from(this.querySelectorAll<SidebarItem>("ak-sidebar-item") || []);
|
||||||
children.forEach(child => child.parent = this);
|
children.forEach((child) => (child.parent = this));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
set activeWhen(regexp: string[]) {
|
set activeWhen(regexp: string[]) {
|
||||||
regexp.forEach(r => {
|
regexp.forEach((r) => {
|
||||||
this.activeMatchers.push(new RegExp(r));
|
this.activeMatchers.push(new RegExp(r));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -110,7 +109,7 @@ export class SidebarItem extends LitElement {
|
||||||
|
|
||||||
onHashChange(): void {
|
onHashChange(): void {
|
||||||
const activePath = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0];
|
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.expandParentRecursive(activePath, item);
|
||||||
});
|
});
|
||||||
this.isActive = this.matchesPath(activePath);
|
this.isActive = this.matchesPath(activePath);
|
||||||
|
@ -125,7 +124,7 @@ export class SidebarItem extends LitElement {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.activeMatchers.some(v => {
|
return this.activeMatchers.some((v) => {
|
||||||
const match = v.exec(path);
|
const match = v.exec(path);
|
||||||
if (match !== null) {
|
if (match !== null) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -138,7 +137,7 @@ export class SidebarItem extends LitElement {
|
||||||
item.parent.expanded = true;
|
item.parent.expanded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
item.childItems.forEach(i => this.expandParentRecursive(activePath, i));
|
item.childItems.forEach((i) => this.expandParentRecursive(activePath, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -153,10 +152,16 @@ export class SidebarItem extends LitElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.childItems.length > 0) {
|
if (this.childItems.length > 0) {
|
||||||
return html`<li class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}">
|
return html`<li
|
||||||
<button class="pf-c-nav__link" aria-expanded="true" @click=${() => {
|
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;
|
this.expanded = !this.expanded;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
<span class="pf-c-nav__toggle">
|
<span class="pf-c-nav__toggle">
|
||||||
<span class="pf-c-nav__toggle-icon">
|
<span class="pf-c-nav__toggle-icon">
|
||||||
|
@ -172,11 +177,16 @@ export class SidebarItem extends LitElement {
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
return html`<li class="pf-c-nav__item">
|
return html`<li class="pf-c-nav__item">
|
||||||
${this.path ? html`
|
${this.path
|
||||||
<a href="${this.isAbsoluteLink ? "" : "#"}${this.path}" class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}">
|
? html`
|
||||||
|
<a
|
||||||
|
href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
|
||||||
|
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
|
||||||
|
>
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
</a>
|
</a>
|
||||||
` : html`
|
`
|
||||||
|
: html`
|
||||||
<span class="pf-c-nav__link">
|
<span class="pf-c-nav__link">
|
||||||
<slot name="label"></slot>
|
<slot name="label"></slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-sidebar-user")
|
@customElement("ak-sidebar-user")
|
||||||
export class SidebarUser extends LitElement {
|
export class SidebarUser extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return [
|
||||||
PFBase,
|
PFBase,
|
||||||
|
@ -34,9 +33,16 @@ export class SidebarUser extends LitElement {
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings">
|
<a href="#/user" class="pf-c-nav__link user-avatar" id="user-settings">
|
||||||
${until(me().then((u) => {
|
${until(
|
||||||
return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`;
|
me().then((u) => {
|
||||||
}), html``)}
|
return html`<img
|
||||||
|
class="pf-c-avatar"
|
||||||
|
src="${ifDefined(u.user.avatar)}"
|
||||||
|
alt=""
|
||||||
|
/>`;
|
||||||
|
}),
|
||||||
|
html``,
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
|
<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>
|
<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";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
export class TableColumn {
|
export class TableColumn {
|
||||||
|
|
||||||
title: string;
|
title: string;
|
||||||
orderBy?: string;
|
orderBy?: string;
|
||||||
|
|
||||||
|
@ -55,8 +54,10 @@ export class TableColumn {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSortable(table: Table<unknown>): TemplateResult {
|
renderSortable(table: Table<unknown>): TemplateResult {
|
||||||
return html`
|
return html` <button
|
||||||
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
|
class="pf-c-table__button"
|
||||||
|
@click=${() => this.headerClickHandler(table)}
|
||||||
|
>
|
||||||
<div class="pf-c-table__button-content">
|
<div class="pf-c-table__button-content">
|
||||||
<span class="pf-c-table__text">${this.title}</span>
|
<span class="pf-c-table__text">${this.title}</span>
|
||||||
<span class="pf-c-table__sort-indicator">
|
<span class="pf-c-table__sort-indicator">
|
||||||
|
@ -72,12 +73,14 @@ export class TableColumn {
|
||||||
scope="col"
|
scope="col"
|
||||||
class="
|
class="
|
||||||
${this.orderBy ? "pf-c-table__sort " : " "}
|
${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}`}
|
${this.orderBy ? this.renderSortable(table) : html`${this.title}`}
|
||||||
</th>`;
|
</th>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Table<T> extends LitElement {
|
export abstract class Table<T> extends LitElement {
|
||||||
|
@ -99,32 +102,41 @@ export abstract class Table<T> extends LitElement {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
data?: AKResponse<T>;
|
data?: AKResponse<T>;
|
||||||
|
|
||||||
@property({type: Number})
|
@property({ type: Number })
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|
||||||
@property({type: String})
|
@property({ type: String })
|
||||||
order?: string;
|
order?: string;
|
||||||
|
|
||||||
@property({type: String})
|
@property({ type: String })
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
checkbox = false;
|
checkbox = false;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
selectedElements: T[] = [];
|
selectedElements: T[] = [];
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
expandable = false;
|
expandable = false;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
expandedRows: boolean[] = [];
|
expandedRows: boolean[] = [];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFTable, PFBullseye, PFButton, PFToolbar, PFDropdown, PFPagination, AKGlobal];
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFTable,
|
||||||
|
PFBullseye,
|
||||||
|
PFButton,
|
||||||
|
PFToolbar,
|
||||||
|
PFDropdown,
|
||||||
|
PFPagination,
|
||||||
|
AKGlobal,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -139,12 +151,14 @@ export abstract class Table<T> extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.apiEndpoint(this.page).then((r) => {
|
this.apiEndpoint(this.page)
|
||||||
|
.then((r) => {
|
||||||
this.data = r;
|
this.data = r;
|
||||||
this.page = r.pagination.current;
|
this.page = r.pagination.current;
|
||||||
this.expandedRows = [];
|
this.expandedRows = [];
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,10 +167,7 @@ export abstract class Table<T> extends LitElement {
|
||||||
return html`<tr role="row">
|
return html`<tr role="row">
|
||||||
<td role="cell" colspan="25">
|
<td role="cell" colspan="25">
|
||||||
<div class="pf-l-bullseye">
|
<div class="pf-l-bullseye">
|
||||||
<ak-empty-state
|
<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
@ -167,7 +178,11 @@ export abstract class Table<T> extends LitElement {
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
<td role="cell" colspan="8">
|
<td role="cell" colspan="8">
|
||||||
<div class="pf-l-bullseye">
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -182,13 +197,18 @@ export abstract class Table<T> extends LitElement {
|
||||||
return [this.renderEmpty()];
|
return [this.renderEmpty()];
|
||||||
}
|
}
|
||||||
return this.data.results.map((item: T, idx: number) => {
|
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;
|
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">
|
<tr role="row">
|
||||||
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
|
${this.checkbox
|
||||||
<input type="checkbox"
|
? html`<td class="pf-c-table__check" role="cell">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
?checked=${this.selectedElements.indexOf(item) >= 0}
|
?checked=${this.selectedElements.indexOf(item) >= 0}
|
||||||
@input=${(ev: InputEvent) => {
|
@input=${(ev: InputEvent) => {
|
||||||
if ((ev.target as HTMLInputElement).checked) {
|
if ((ev.target as HTMLInputElement).checked) {
|
||||||
|
@ -201,21 +221,38 @@ export abstract class Table<T> extends LitElement {
|
||||||
this.selectedElements.splice(index, 1);
|
this.selectedElements.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}} />
|
}}
|
||||||
</td>` : html``}
|
/>
|
||||||
${this.expandable ? html`<td class="pf-c-table__toggle" role="cell">
|
</td>`
|
||||||
<button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => {
|
: 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.expandedRows[idx] = !this.expandedRows[idx];
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}}>
|
}}
|
||||||
<div class="pf-c-table__toggle-icon"> <i class="fas fa-angle-down" aria-hidden="true"></i> </div>
|
>
|
||||||
|
<div class="pf-c-table__toggle-icon">
|
||||||
|
<i class="fas fa-angle-down" aria-hidden="true"></i
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</td>` : html``}
|
</td>`
|
||||||
|
: html``}
|
||||||
${this.row(item).map((col) => {
|
${this.row(item).map((col) => {
|
||||||
return html`<td role="cell">${col}</td>`;
|
return html`<td role="cell">${col}</td>`;
|
||||||
})}
|
})}
|
||||||
</tr>
|
</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>
|
<td></td>
|
||||||
${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
|
${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -230,10 +267,11 @@ export abstract class Table<T> extends LitElement {
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
class="pf-c-button pf-m-primary">
|
class="pf-c-button pf-m-primary"
|
||||||
|
>
|
||||||
${t`Refresh`}
|
${t`Refresh`}
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
@ -246,16 +284,20 @@ export abstract class Table<T> extends LitElement {
|
||||||
if (!this.searchEnabled()) {
|
if (!this.searchEnabled()) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
return html`<ak-table-search
|
||||||
|
value=${ifDefined(this.search)}
|
||||||
|
.onSearch=${(value: string) => {
|
||||||
this.search = value;
|
this.search = value;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
</ak-table-search> `;
|
>
|
||||||
|
</ak-table-search
|
||||||
|
> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ -268,20 +310,17 @@ export abstract class Table<T> extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTable(): TemplateResult {
|
renderTable(): TemplateResult {
|
||||||
return html`
|
return html` ${this.checkbox
|
||||||
${this.checkbox ?
|
? html`<ak-chip-group>
|
||||||
html`<ak-chip-group>
|
${this.selectedElements.map((el) => {
|
||||||
${this.selectedElements.map(el => {
|
|
||||||
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
|
return html`<ak-chip>${this.renderSelectedChip(el)}</ak-chip>`;
|
||||||
})}
|
})}
|
||||||
</ak-chip-group>`:
|
</ak-chip-group>`
|
||||||
html``}
|
: html``}
|
||||||
<div class="pf-c-toolbar">
|
<div class="pf-c-toolbar">
|
||||||
<div class="pf-c-toolbar__content">
|
<div class="pf-c-toolbar__content">
|
||||||
${this.renderSearch()}
|
${this.renderSearch()}
|
||||||
<div class="pf-c-toolbar__bulk-select">
|
<div class="pf-c-toolbar__bulk-select">${this.renderToolbar()}</div>
|
||||||
${this.renderToolbar()}
|
|
||||||
</div>
|
|
||||||
${this.renderToolbarAfter()}
|
${this.renderToolbarAfter()}
|
||||||
<ak-table-pagination
|
<ak-table-pagination
|
||||||
class="pf-c-toolbar__item pf-m-pagination"
|
class="pf-c-toolbar__item pf-m-pagination"
|
||||||
|
@ -292,29 +331,36 @@ export abstract class Table<T> extends LitElement {
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
</ak-table-pagination>
|
</ak-table-pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
|
<table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
${this.checkbox ? html`<td class="pf-c-table__check" role="cell">
|
${this.checkbox
|
||||||
<input type="checkbox" aria-label=${t`Select all rows`} @input=${(ev: InputEvent) => {
|
? 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) {
|
if ((ev.target as HTMLInputElement).checked) {
|
||||||
this.selectedElements = this.data?.results || [];
|
this.selectedElements = this.data?.results || [];
|
||||||
} else {
|
} else {
|
||||||
this.selectedElements = [];
|
this.selectedElements = [];
|
||||||
}
|
}
|
||||||
}} />
|
}}
|
||||||
</td>` : html``}
|
/>
|
||||||
|
</td>`
|
||||||
|
: html``}
|
||||||
${this.expandable ? html`<td role="cell"></td>` : html``}
|
${this.expandable ? html`<td role="cell"></td>` : html``}
|
||||||
${this.columns().map((col) => col.render(this))}
|
${this.columns().map((col) => col.render(this))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
${(this.isLoading || !this.data) ? this.renderLoading() : this.renderRows()}
|
${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
|
||||||
</table>
|
</table>
|
||||||
<div class="pf-c-pagination pf-m-bottom">
|
<div class="pf-c-pagination pf-m-bottom">
|
||||||
<ak-table-pagination
|
<ak-table-pagination
|
||||||
|
@ -326,9 +372,10 @@ export abstract class Table<T> extends LitElement {
|
||||||
new CustomEvent(EVENT_REFRESH, {
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
</ak-table-pagination>
|
</ak-table-pagination>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,16 @@ export abstract class TableModal<T> extends Table<T> {
|
||||||
open = false;
|
open = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
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() {
|
constructor() {
|
||||||
|
@ -33,7 +42,7 @@ export abstract class TableModal<T> extends Table<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetForms(): void {
|
resetForms(): void {
|
||||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach(form => {
|
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
|
||||||
if ("resetForm" in form) {
|
if ("resetForm" in form) {
|
||||||
form?.resetForm();
|
form?.resetForm();
|
||||||
}
|
}
|
||||||
|
@ -42,7 +51,7 @@ export abstract class TableModal<T> extends Table<T> {
|
||||||
|
|
||||||
onClick(): void {
|
onClick(): void {
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.querySelectorAll("*").forEach(child => {
|
this.querySelectorAll("*").forEach((child) => {
|
||||||
if ("requestUpdate" in child) {
|
if ("requestUpdate" in child) {
|
||||||
(child as LitElement).requestUpdate();
|
(child as LitElement).requestUpdate();
|
||||||
}
|
}
|
||||||
|
@ -56,11 +65,7 @@ export abstract class TableModal<T> extends Table<T> {
|
||||||
renderModal(): TemplateResult {
|
renderModal(): TemplateResult {
|
||||||
return html`<div class="pf-c-backdrop">
|
return html`<div class="pf-c-backdrop">
|
||||||
<div class="pf-l-bullseye">
|
<div class="pf-l-bullseye">
|
||||||
<div
|
<div class="pf-c-modal-box ${this.size}" role="dialog" aria-modal="true">
|
||||||
class="pf-c-modal-box ${this.size}"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
@click=${() => (this.open = false)}
|
@click=${() => (this.open = false)}
|
||||||
class="pf-c-button pf-m-plain"
|
class="pf-c-button pf-m-plain"
|
||||||
|
|
|
@ -19,7 +19,8 @@ export abstract class TablePage<T> extends Table<T> {
|
||||||
return html`<ak-page-header
|
return html`<ak-page-header
|
||||||
icon=${this.pageIcon()}
|
icon=${this.pageIcon()}
|
||||||
header=${this.pageTitle()}
|
header=${this.pageTitle()}
|
||||||
description=${ifDefined(this.pageDescription())}>
|
description=${ifDefined(this.pageDescription())}
|
||||||
|
>
|
||||||
</ak-page-header>
|
</ak-page-header>
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||||
<div class="pf-c-card">${this.renderTable()}</div>
|
<div class="pf-c-card">${this.renderTable()}</div>
|
||||||
|
|
|
@ -8,12 +8,12 @@ import AKGlobal from "../../authentik.css";
|
||||||
|
|
||||||
@customElement("ak-table-pagination")
|
@customElement("ak-table-pagination")
|
||||||
export class TablePagination extends LitElement {
|
export class TablePagination extends LitElement {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
pages?: AKPagination;
|
pages?: AKPagination;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
pageChangeHandler: (page: number) => void = (page: number) => {}
|
pageChangeHandler: (page: number) => void = (page: number) => {};
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFButton, PFPagination, AKGlobal];
|
return [PFBase, PFButton, PFPagination, AKGlobal];
|
||||||
|
@ -33,7 +33,9 @@ export class TablePagination extends LitElement {
|
||||||
<div class="pf-c-pagination__nav-control pf-m-prev">
|
<div class="pf-c-pagination__nav-control pf-m-prev">
|
||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-plain"
|
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}"
|
?disabled="${(this.pages?.previous || 0) < 1}"
|
||||||
aria-label="${t`Go to previous page`}"
|
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">
|
<div class="pf-c-pagination__nav-control pf-m-next">
|
||||||
<button
|
<button
|
||||||
class="pf-c-button pf-m-plain"
|
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}"
|
?disabled="${(this.pages?.next || 0) <= 0}"
|
||||||
aria-label="${t`Go to next page`}"
|
aria-label="${t`Go to next page`}"
|
||||||
>
|
>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { t } from "@lingui/macro";
|
||||||
|
|
||||||
@customElement("ak-table-search")
|
@customElement("ak-table-search")
|
||||||
export class TableSearch extends LitElement {
|
export class TableSearch extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
||||||
|
@ -24,18 +23,30 @@ export class TableSearch extends LitElement {
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||||
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
|
<form
|
||||||
|
class="pf-c-input-group"
|
||||||
|
method="GET"
|
||||||
|
@submit=${(e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.onSearch) return;
|
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) return;
|
||||||
if (el.value === "") return;
|
if (el.value === "") return;
|
||||||
this.onSearch(el?.value);
|
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;
|
if (!this.onSearch) return;
|
||||||
this.onSearch((ev.target as HTMLInputElement).value);
|
this.onSearch((ev.target as HTMLInputElement).value);
|
||||||
}}>
|
}}
|
||||||
|
/>
|
||||||
<button class="pf-c-button pf-m-control" type="submit">
|
<button class="pf-c-button pf-m-control" type="submit">
|
||||||
<i class="fas fa-search" aria-hidden="true"></i>
|
<i class="fas fa-search" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -43,5 +54,4 @@ export class TableSearch extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
|
||||||
@customElement("ak-user-session-list")
|
@customElement("ak-user-session-list")
|
||||||
export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
targetUser!: string;
|
targetUser!: string;
|
||||||
|
|
||||||
|
@ -41,8 +40,7 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
||||||
html`${item.userAgent.userAgent?.family}`,
|
html`${item.userAgent.userAgent?.family}`,
|
||||||
html`${item.userAgent.os?.family}`,
|
html`${item.userAgent.os?.family}`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires?.toLocaleString()}`,
|
||||||
html`
|
html` <ak-forms-delete
|
||||||
<ak-forms-delete
|
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${t`Session`}
|
objectLabel=${t`Session`}
|
||||||
.usedBy=${() => {
|
.usedBy=${() => {
|
||||||
|
@ -54,12 +52,10 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
|
return new CoreApi(DEFAULT_CONFIG).coreAuthenticatedSessionsDestroy({
|
||||||
uuid: item.uuid || "",
|
uuid: item.uuid || "",
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
>
|
||||||
${t`Delete Session`}
|
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Session`}</button>
|
||||||
</button>
|
|
||||||
</ak-forms-delete>`,
|
</ak-forms-delete>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,25 +36,22 @@ export class UserConsentList extends Table<UserConsent> {
|
||||||
return [
|
return [
|
||||||
html`${item.application.name}`,
|
html`${item.application.name}`,
|
||||||
html`${item.expires?.toLocaleString()}`,
|
html`${item.expires?.toLocaleString()}`,
|
||||||
html`
|
html` <ak-forms-delete
|
||||||
<ak-forms-delete
|
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${t`Consent`}
|
objectLabel=${t`Consent`}
|
||||||
.usedBy=${() => {
|
.usedBy=${() => {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({
|
return new CoreApi(DEFAULT_CONFIG).coreUserConsentUsedByList({
|
||||||
id: item.pk
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.delete=${() => {
|
.delete=${() => {
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({
|
return new CoreApi(DEFAULT_CONFIG).coreUserConsentDestroy({
|
||||||
id: item.pk,
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
<button slot="trigger" class="pf-c-button pf-m-danger">
|
>
|
||||||
${t`Delete Consent`}
|
<button slot="trigger" class="pf-c-button pf-m-danger">${t`Delete Consent`}</button>
|
||||||
</button>
|
|
||||||
</ak-forms-delete>`,
|
</ak-forms-delete>`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import { t } from "@lingui/macro";
|
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 PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
@ -26,7 +34,15 @@ import "./stages/password/PasswordStage";
|
||||||
import "./stages/prompt/PromptStage";
|
import "./stages/prompt/PromptStage";
|
||||||
import "./sources/plex/PlexLoginInit";
|
import "./sources/plex/PlexLoginInit";
|
||||||
import { StageHost } from "./stages/base";
|
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 { DEFAULT_CONFIG, tenant } from "../api/Config";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
|
@ -37,13 +53,12 @@ import { WebsocketClient } from "../common/ws";
|
||||||
|
|
||||||
@customElement("ak-flow-executor")
|
@customElement("ak-flow-executor")
|
||||||
export class FlowExecutor extends LitElement implements StageHost {
|
export class FlowExecutor extends LitElement implements StageHost {
|
||||||
|
|
||||||
flowSlug: string;
|
flowSlug: string;
|
||||||
|
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
challenge?: ChallengeTypes;
|
challenge?: ChallengeTypes;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
|
@ -84,13 +99,15 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
}
|
}
|
||||||
|
|
||||||
setBackground(url: string): void {
|
setBackground(url: string): void {
|
||||||
this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
|
this.shadowRoot
|
||||||
|
?.querySelectorAll<HTMLDivElement>(".pf-c-background-image")
|
||||||
|
.forEach((bg) => {
|
||||||
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
|
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private postUpdate(): void {
|
private postUpdate(): void {
|
||||||
tenant().then(tenant => {
|
tenant().then((tenant) => {
|
||||||
if (this.challenge?.flowInfo?.title) {
|
if (this.challenge?.flowInfo?.title) {
|
||||||
document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
|
document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,38 +122,46 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
payload.component = this.challenge.component;
|
payload.component = this.challenge.component;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
return new FlowsApi(DEFAULT_CONFIG)
|
||||||
|
.flowsExecutorSolve({
|
||||||
flowSlug: this.flowSlug,
|
flowSlug: this.flowSlug,
|
||||||
query: window.location.search.substring(1),
|
query: window.location.search.substring(1),
|
||||||
flowChallengeResponseRequest: payload,
|
flowChallengeResponseRequest: payload,
|
||||||
}).then((data) => {
|
})
|
||||||
|
.then((data) => {
|
||||||
this.challenge = data;
|
this.challenge = data;
|
||||||
this.postUpdate();
|
this.postUpdate();
|
||||||
}).catch((e: Error | Response) => {
|
})
|
||||||
|
.catch((e: Error | Response) => {
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
}).finally(() => {
|
})
|
||||||
|
.finally(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
configureSentry();
|
configureSentry();
|
||||||
tenant().then(tenant => this.tenant = tenant);
|
tenant().then((tenant) => (this.tenant = tenant));
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
|
new FlowsApi(DEFAULT_CONFIG)
|
||||||
|
.flowsExecutorGet({
|
||||||
flowSlug: this.flowSlug,
|
flowSlug: this.flowSlug,
|
||||||
query: window.location.search.substring(1),
|
query: window.location.search.substring(1),
|
||||||
}).then((challenge) => {
|
})
|
||||||
|
.then((challenge) => {
|
||||||
this.challenge = challenge;
|
this.challenge = challenge;
|
||||||
// Only set background on first update, flow won't change throughout execution
|
// Only set background on first update, flow won't change throughout execution
|
||||||
if (this.challenge?.flowInfo?.background) {
|
if (this.challenge?.flowInfo?.background) {
|
||||||
this.setBackground(this.challenge.flowInfo.background);
|
this.setBackground(this.challenge.flowInfo.background);
|
||||||
}
|
}
|
||||||
this.postUpdate();
|
this.postUpdate();
|
||||||
}).catch((e: Error | Response) => {
|
})
|
||||||
|
.catch((e: Error | Response) => {
|
||||||
// Catch JSON or Update errors
|
// Catch JSON or Update errors
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
}).finally(() => {
|
})
|
||||||
|
.finally(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -167,7 +192,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</footer>`
|
</footer>`,
|
||||||
} as ChallengeTypes;
|
} as ChallengeTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,46 +208,92 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
}
|
}
|
||||||
switch (this.challenge.type) {
|
switch (this.challenge.type) {
|
||||||
case ChallengeChoices.Redirect:
|
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);
|
window.location.assign((this.challenge as RedirectChallenge).to);
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
|
||||||
?loading=${true}
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
</ak-empty-state>`;
|
||||||
case ChallengeChoices.Shell:
|
case ChallengeChoices.Shell:
|
||||||
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
|
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
|
||||||
case ChallengeChoices.Native:
|
case ChallengeChoices.Native:
|
||||||
switch (this.challenge.component) {
|
switch (this.challenge.component) {
|
||||||
case "ak-stage-access-denied":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -236,26 +307,38 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
|
|
||||||
renderChallengeWrapper(): TemplateResult {
|
renderChallengeWrapper(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading=${true}
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`
|
return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `;
|
||||||
${this.loading ? this.renderLoading() : html``}
|
|
||||||
${this.renderChallenge()}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-background-image">
|
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">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="pf-c-background-image__filter"
|
||||||
|
width="0"
|
||||||
|
height="0"
|
||||||
|
>
|
||||||
<filter id="image_overlay">
|
<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" />
|
<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">
|
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
<feFuncR
|
||||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
type="table"
|
||||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
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>
|
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||||
</feComponentTransfer>
|
</feComponentTransfer>
|
||||||
</filter>
|
</filter>
|
||||||
|
@ -265,30 +348,44 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
<div class="ak-login-container">
|
<div class="ak-login-container">
|
||||||
<header class="pf-c-login__header">
|
<header class="pf-c-login__header">
|
||||||
<div class="pf-c-brand ak-brand">
|
<div class="pf-c-brand ak-brand">
|
||||||
<img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
|
<img
|
||||||
|
src="${ifDefined(this.tenant?.brandingLogo)}"
|
||||||
|
alt="authentik icon"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main">
|
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
|
||||||
${this.renderChallengeWrapper()}
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__footer">
|
<footer class="pf-c-login__footer">
|
||||||
<p></p>
|
<p></p>
|
||||||
<ul class="pf-c-list pf-m-inline">
|
<ul class="pf-c-list pf-m-inline">
|
||||||
${until(this.tenant?.uiFooterLinks?.map((link) => {
|
${until(
|
||||||
|
this.tenant?.uiFooterLinks?.map((link) => {
|
||||||
return html`<li>
|
return html`<li>
|
||||||
<a href="${link.href || ""}">${link.name}</a>
|
<a href="${link.href || ""}">${link.name}</a>
|
||||||
</li>`;
|
</li>`;
|
||||||
}))}
|
}),
|
||||||
${this.tenant?.brandingTitle != "authentik" ? html`
|
)}
|
||||||
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
|
${this.tenant?.brandingTitle != "authentik"
|
||||||
` : html``}
|
? html`
|
||||||
${this.challenge?.flowInfo?.background?.startsWith("/static") ? html`
|
<li>
|
||||||
<li><a href="https://unsplash.com/@ventiviews">${t`Background image`}</a></li>
|
<a href="https://goauthentik.io"
|
||||||
` : html``}
|
>${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>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { t } from "@lingui/macro";
|
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 PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-form-static")
|
@customElement("ak-form-static")
|
||||||
export class FormStatic extends LitElement {
|
export class FormStatic extends LitElement {
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
userAvatar?: string;
|
userAvatar?: string;
|
||||||
|
|
||||||
|
@ -13,7 +20,9 @@ export class FormStatic extends LitElement {
|
||||||
user = "";
|
user = "";
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFAvatar, css`
|
return [
|
||||||
|
PFAvatar,
|
||||||
|
css`
|
||||||
/* Form with user */
|
/* Form with user */
|
||||||
.form-control-static {
|
.form-control-static {
|
||||||
margin-top: var(--pf-global--spacer--sm);
|
margin-top: var(--pf-global--spacer--sm);
|
||||||
|
@ -33,19 +42,23 @@ export class FormStatic extends LitElement {
|
||||||
padding-bottom: var(--pf-global--spacer--xs);
|
padding-bottom: var(--pf-global--spacer--xs);
|
||||||
line-height: var(--pf-global--spacer--xl);
|
line-height: var(--pf-global--spacer--xl);
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="form-control-static">
|
<div class="form-control-static">
|
||||||
<div class="avatar">
|
<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}
|
${this.user}
|
||||||
</div>
|
</div>
|
||||||
<slot name="link"></slot>
|
<slot name="link"></slot>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,23 +13,20 @@ import { t } from "@lingui/macro";
|
||||||
import "../../elements/EmptyState";
|
import "../../elements/EmptyState";
|
||||||
|
|
||||||
@customElement("ak-stage-access-denied")
|
@customElement("ak-stage-access-denied")
|
||||||
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
|
export class FlowAccessDenied extends BaseStage<
|
||||||
|
AccessDeniedChallenge,
|
||||||
|
FlowChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form method="POST" class="pf-c-form">
|
<form method="POST" class="pf-c-form">
|
||||||
|
@ -39,15 +36,13 @@ export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChall
|
||||||
${t`Request has been denied.`}
|
${t`Request has been denied.`}
|
||||||
</p>
|
</p>
|
||||||
${this.challenge?.errorMessage &&
|
${this.challenge?.errorMessage &&
|
||||||
html`<hr>
|
html`<hr />
|
||||||
<p>${this.challenge.errorMessage}</p>`}
|
<p>${this.challenge.errorMessage}</p>`}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,40 +23,54 @@ export const DEFAULT_HEADERS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
|
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 top = (screen.height - h) / 4,
|
||||||
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
left = (screen.width - w) / 2;
|
||||||
|
const popup = window.open(
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
`scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`,
|
||||||
|
);
|
||||||
return popup;
|
return popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlexAPIClient {
|
export class PlexAPIClient {
|
||||||
|
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
constructor(token: string) {
|
constructor(token: string) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
|
static async getPin(
|
||||||
const headers = { ...DEFAULT_HEADERS, ...{
|
clientIdentifier: string,
|
||||||
"X-Plex-Client-Identifier": clientIdentifier
|
): 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", {
|
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
const pin: PlexPinResponse = await pinResponse.json();
|
const pin: PlexPinResponse = await pinResponse.json();
|
||||||
return {
|
return {
|
||||||
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
|
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(
|
||||||
pin: pin
|
clientIdentifier,
|
||||||
|
)}&code=${pin.code}`,
|
||||||
|
pin: pin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
|
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
|
||||||
const headers = { ...DEFAULT_HEADERS, ...{
|
const headers = {
|
||||||
"X-Plex-Client-Identifier": clientIdentifier
|
...DEFAULT_HEADERS,
|
||||||
}};
|
...{
|
||||||
|
"X-Plex-Client-Identifier": clientIdentifier,
|
||||||
|
},
|
||||||
|
};
|
||||||
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
|
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
|
||||||
headers: headers
|
headers: headers,
|
||||||
});
|
});
|
||||||
const pin: PlexPinResponse = await pinResponse.json();
|
const pin: PlexPinResponse = await pinResponse.json();
|
||||||
return pin.authToken || "";
|
return pin.authToken || "";
|
||||||
|
@ -65,7 +79,7 @@ export class PlexAPIClient {
|
||||||
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
|
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
|
||||||
const executePoll = async (
|
const executePoll = async (
|
||||||
resolve: (authToken: string) => void,
|
resolve: (authToken: string) => void,
|
||||||
reject: (e: Error) => void
|
reject: (e: Error) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
|
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
|
||||||
|
@ -84,13 +98,15 @@ export class PlexAPIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServers(): Promise<PlexResource[]> {
|
async getServers(): Promise<PlexResource[]> {
|
||||||
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
|
const resourcesResponse = await fetch(
|
||||||
headers: DEFAULT_HEADERS
|
`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();
|
const resources: PlexResource[] = await resourcesResponse.json();
|
||||||
return resources.filter(r => {
|
return resources.filter((r) => {
|
||||||
return r.provides.toLowerCase().includes("server") && r.owned;
|
return r.provides.toLowerCase().includes("server") && r.owned;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { t } from "@lingui/macro";
|
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 PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.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 { showMessage } from "../../../elements/messages/MessageContainer";
|
||||||
import { MessageLevel } from "../../../elements/messages/Message";
|
import { MessageLevel } from "../../../elements/messages/Message";
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-flow-sources-plex")
|
@customElement("ak-flow-sources-plex")
|
||||||
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
|
export class PlexLoginInit extends BaseStage<
|
||||||
|
PlexAuthenticationChallenge,
|
||||||
|
PlexAuthenticationChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -27,20 +31,23 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
|
||||||
async firstUpdated(): Promise<void> {
|
async firstUpdated(): Promise<void> {
|
||||||
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
|
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
|
||||||
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
|
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();
|
authWindow?.close();
|
||||||
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
|
new SourcesApi(DEFAULT_CONFIG)
|
||||||
|
.sourcesPlexRedeemTokenCreate({
|
||||||
plexTokenRedeemRequest: {
|
plexTokenRedeemRequest: {
|
||||||
plexToken: token,
|
plexToken: token,
|
||||||
},
|
},
|
||||||
slug: this.challenge?.slug || "",
|
slug: this.challenge?.slug || "",
|
||||||
}).then((r) => {
|
})
|
||||||
|
.then((r) => {
|
||||||
window.location.assign(r.to);
|
window.location.assign(r.to);
|
||||||
}).catch((r: Response) => {
|
})
|
||||||
r.json().then((body: {detail: string}) => {
|
.catch((r: Response) => {
|
||||||
|
r.json().then((body: { detail: string }) => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: body.detail
|
message: body.detail,
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.assign("/");
|
window.location.assign("/");
|
||||||
|
@ -52,21 +59,15 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Plex...`}</h1>
|
||||||
${t`Authenticating with Plex...`}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form">
|
<form class="pf-c-form">
|
||||||
<ak-empty-state
|
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
||||||
?loading="${true}">
|
|
||||||
</ak-empty-state>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,19 @@ import { BaseStage } from "../base";
|
||||||
import "../../../elements/forms/FormElement";
|
import "../../../elements/forms/FormElement";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import "../../FormStatic";
|
import "../../FormStatic";
|
||||||
import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, StagesApi } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorDuoChallenge,
|
||||||
|
AuthenticatorDuoChallengeResponseRequest,
|
||||||
|
StagesApi,
|
||||||
|
} from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-duo")
|
@customElement("ak-stage-authenticator-duo")
|
||||||
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
|
export class AuthenticatorDuoStage extends BaseStage<
|
||||||
|
AuthenticatorDuoChallenge,
|
||||||
|
AuthenticatorDuoChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -31,35 +37,41 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEnrollStatus(): Promise<void> {
|
checkEnrollStatus(): Promise<void> {
|
||||||
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
|
return new StagesApi(DEFAULT_CONFIG)
|
||||||
|
.stagesAuthenticatorDuoEnrollmentStatusCreate({
|
||||||
stageUuid: this.challenge?.stageUuid || "",
|
stageUuid: this.challenge?.stageUuid || "",
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
this.host?.submit({});
|
this.host?.submit({});
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
console.debug("authentik/flows/duo: Waiting for auth status");
|
console.debug("authentik/flows/duo: Waiting for auth status");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<img src=${this.challenge.activationBarcode} />
|
<img src=${this.challenge.activationBarcode} />
|
||||||
|
@ -69,18 +81,20 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
|
||||||
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
|
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pf-c-button pf-m-primary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
this.checkEnrollStatus();
|
this.checkEnrollStatus();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${t`Check status`}
|
${t`Check status`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,58 +11,75 @@ import { BaseStage } from "../base";
|
||||||
import "../../../elements/forms/FormElement";
|
import "../../../elements/forms/FormElement";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import "../../FormStatic";
|
import "../../FormStatic";
|
||||||
import { AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorStaticChallenge,
|
||||||
|
AuthenticatorStaticChallengeResponseRequest,
|
||||||
|
} from "authentik-api";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
export const STATIC_TOKEN_STYLE = css`
|
export const STATIC_TOKEN_STYLE = css`
|
||||||
/* Static OTP Tokens */
|
/* Static OTP Tokens */
|
||||||
.ak-otp-tokens {
|
.ak-otp-tokens {
|
||||||
list-style: circle;
|
list-style: circle;
|
||||||
columns: 2;
|
columns: 2;
|
||||||
-webkit-columns: 2;
|
-webkit-columns: 2;
|
||||||
-moz-columns: 2;
|
-moz-columns: 2;
|
||||||
margin-left: var(--pf-global--spacer--xs);
|
margin-left: var(--pf-global--spacer--xs);
|
||||||
}
|
}
|
||||||
.ak-otp-tokens li {
|
.ak-otp-tokens li {
|
||||||
font-size: var(--pf-global--FontSize--2xl);
|
font-size: var(--pf-global--FontSize--2xl);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-static")
|
@customElement("ak-stage-authenticator-static")
|
||||||
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
|
export class AuthenticatorStaticStage extends BaseStage<
|
||||||
|
AuthenticatorStaticChallenge,
|
||||||
|
AuthenticatorStaticChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
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 {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<ak-form-element
|
<ak-form-element
|
||||||
label="${t`Tokens`}"
|
label="${t`Tokens`}"
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group">
|
class="pf-c-form__group"
|
||||||
|
>
|
||||||
<ul class="ak-otp-tokens">
|
<ul class="ak-otp-tokens">
|
||||||
${this.challenge.codes.map((token) => {
|
${this.challenge.codes.map((token) => {
|
||||||
return html`<li>${token}</li>`;
|
return html`<li>${token}</li>`;
|
||||||
|
@ -78,9 +95,7 @@ export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChall
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,53 +14,66 @@ import { showMessage } from "../../../elements/messages/MessageContainer";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import "../../FormStatic";
|
import "../../FormStatic";
|
||||||
import { MessageLevel } from "../../../elements/messages/Message";
|
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";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-totp")
|
@customElement("ak-stage-authenticator-totp")
|
||||||
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
|
export class AuthenticatorTOTPStage extends BaseStage<
|
||||||
|
AuthenticatorTOTPChallenge,
|
||||||
|
AuthenticatorTOTPChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
|
||||||
<ak-form-element>
|
<ak-form-element>
|
||||||
<!-- @ts-ignore -->
|
<!-- @ts-ignore -->
|
||||||
<qr-code data="${this.challenge.configUrl}"></qr-code>
|
<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) => {
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress"
|
||||||
|
@click=${(e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.challenge?.configUrl) return;
|
if (!this.challenge?.configUrl) return;
|
||||||
navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
|
navigator.clipboard
|
||||||
|
.writeText(this.challenge?.configUrl)
|
||||||
|
.then(() => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.success,
|
level: MessageLevel.success,
|
||||||
message: t`Successfully copied TOTP Config.`
|
message: t`Successfully copied TOTP Config.`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
|
<span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
|
||||||
${t`Copy`}
|
${t`Copy`}
|
||||||
</button>
|
</button>
|
||||||
|
@ -69,9 +82,11 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
||||||
label="${t`Code`}"
|
label="${t`Code`}"
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})["code"]}>
|
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||||
|
>
|
||||||
<!-- @ts-ignore -->
|
<!-- @ts-ignore -->
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
name="code"
|
name="code"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
|
@ -79,7 +94,8 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
||||||
autofocus=""
|
autofocus=""
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</ak-form-element>
|
</ak-form-element>
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
@ -90,9 +106,7 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,11 @@ import "./AuthenticatorValidateStageWebAuthn";
|
||||||
import "./AuthenticatorValidateStageCode";
|
import "./AuthenticatorValidateStageCode";
|
||||||
import "./AuthenticatorValidateStageDuo";
|
import "./AuthenticatorValidateStageDuo";
|
||||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
||||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
DeviceChallenge,
|
||||||
|
} from "authentik-api";
|
||||||
|
|
||||||
export enum DeviceClasses {
|
export enum DeviceClasses {
|
||||||
STATIC = "static",
|
STATIC = "static",
|
||||||
|
@ -22,9 +26,14 @@ export enum DeviceClasses {
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate")
|
@customElement("ak-stage-authenticator-validate")
|
||||||
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
|
export class AuthenticatorValidateStage
|
||||||
|
extends BaseStage<
|
||||||
@property({attribute: false})
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
|
>
|
||||||
|
implements StageHost
|
||||||
|
{
|
||||||
|
@property({ attribute: false })
|
||||||
selectedDeviceChallenge?: DeviceChallenge;
|
selectedDeviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
|
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
|
||||||
|
@ -65,7 +74,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
||||||
return html`<i class="fas fa-mobile-alt"></i>
|
return html`<i class="fas fa-mobile-alt"></i>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<p>${t`Duo push-notifications`}</p>
|
<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>`;
|
</div>`;
|
||||||
case DeviceClasses.WEBAUTHN:
|
case DeviceClasses.WEBAUTHN:
|
||||||
return html`<i class="fas fa-mobile-alt"></i>
|
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,
|
// and we have a pre-filled value from the password manager,
|
||||||
// directly set the the TOTP device Challenge as active.
|
// directly set the the TOTP device Challenge as active.
|
||||||
if (PasswordManagerPrefill.totp) {
|
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;
|
this.selectedDeviceChallenge = deviceChallenge;
|
||||||
// Delay the update as a re-render isn't triggered from here
|
// Delay the update as a re-render isn't triggered from here
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -103,13 +116,16 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDevicePicker(): TemplateResult {
|
renderDevicePicker(): TemplateResult {
|
||||||
return html`
|
return html` <ul>
|
||||||
<ul>
|
|
||||||
${this.challenge?.deviceChallenges.map((challenges) => {
|
${this.challenge?.deviceChallenges.map((challenges) => {
|
||||||
return html`<li>
|
return html`<li>
|
||||||
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button authenticator-button"
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
this.selectedDeviceChallenge = challenges;
|
this.selectedDeviceChallenge = challenges;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${this.renderDevicePickerSingle(challenges)}
|
${this.renderDevicePickerSingle(challenges)}
|
||||||
</button>
|
</button>
|
||||||
</li>`;
|
</li>`;
|
||||||
|
@ -128,21 +144,24 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
||||||
.host=${this}
|
.host=${this}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||||
|
>
|
||||||
</ak-stage-authenticator-validate-code>`;
|
</ak-stage-authenticator-validate-code>`;
|
||||||
case DeviceClasses.WEBAUTHN:
|
case DeviceClasses.WEBAUTHN:
|
||||||
return html`<ak-stage-authenticator-validate-webauthn
|
return html`<ak-stage-authenticator-validate-webauthn
|
||||||
.host=${this}
|
.host=${this}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||||
|
>
|
||||||
</ak-stage-authenticator-validate-webauthn>`;
|
</ak-stage-authenticator-validate-webauthn>`;
|
||||||
case DeviceClasses.DUO:
|
case DeviceClasses.DUO:
|
||||||
return html`<ak-stage-authenticator-validate-duo
|
return html`<ak-stage-authenticator-validate-duo
|
||||||
.host=${this}
|
.host=${this}
|
||||||
.challenge=${this.challenge}
|
.challenge=${this.challenge}
|
||||||
.deviceChallenge=${this.selectedDeviceChallenge}
|
.deviceChallenge=${this.selectedDeviceChallenge}
|
||||||
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
|
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
|
||||||
|
>
|
||||||
</ak-stage-authenticator-validate-duo>`;
|
</ak-stage-authenticator-validate-duo>`;
|
||||||
}
|
}
|
||||||
return html``;
|
return html``;
|
||||||
|
@ -150,32 +169,25 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </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
|
// User only has a single device class, so we don't show a picker
|
||||||
if (this.challenge?.deviceChallenges.length === 1) {
|
if (this.challenge?.deviceChallenges.length === 1) {
|
||||||
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
|
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
${this.selectedDeviceChallenge
|
||||||
</h1>
|
? ""
|
||||||
${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
|
: html`<p class="pf-c-login__main-header-desc">
|
||||||
${t`Select an identification method.`}
|
${t`Select an identification method.`}
|
||||||
</p>`}
|
</p>`}
|
||||||
</header>
|
</header>
|
||||||
${this.selectedDeviceChallenge ?
|
${this.selectedDeviceChallenge
|
||||||
this.renderDeviceChallenge() :
|
? this.renderDeviceChallenge()
|
||||||
html`<div class="pf-c-login__main-body">
|
: html`<div class="pf-c-login__main-body">${this.renderDevicePicker()}</div>
|
||||||
${this.renderDevicePicker()}
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`}`;
|
</footer>`}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,18 @@ import "../../../elements/forms/FormElement";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
|
||||||
import "../../FormStatic";
|
import "../../FormStatic";
|
||||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
DeviceChallenge,
|
||||||
|
} from "authentik-api";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-code")
|
@customElement("ak-stage-authenticator-validate-code")
|
||||||
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
export class AuthenticatorValidateStageWebCode extends BaseStage<
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
|
> {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
deviceChallenge?: DeviceChallenge;
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
@ -31,28 +37,35 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<ak-form-element
|
<ak-form-element
|
||||||
label="${t`Code`}"
|
label="${t`Code`}"
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})["code"]}>
|
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||||
|
>
|
||||||
<!-- @ts-ignore -->
|
<!-- @ts-ignore -->
|
||||||
<input type="text"
|
<input
|
||||||
|
type="text"
|
||||||
name="code"
|
name="code"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
|
@ -61,7 +74,8 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
value="${PasswordManagerPrefill.totp || ""}"
|
value="${PasswordManagerPrefill.totp || ""}"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</ak-form-element>
|
</ak-form-element>
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
@ -73,18 +87,22 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links">
|
||||||
${this.showBackButton ?
|
${this.showBackButton
|
||||||
html`<li class="pf-c-login__main-footer-links-item">
|
? html`<li class="pf-c-login__main-footer-links-item">
|
||||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
if (!this.host) return;
|
if (!this.host) return;
|
||||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
(
|
||||||
}}>
|
this.host as AuthenticatorValidateStage
|
||||||
|
).selectedDeviceChallenge = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
${t`Return to device picker`}
|
${t`Return to device picker`}
|
||||||
</button>
|
</button>
|
||||||
</li>`:
|
</li>`
|
||||||
html``}
|
: html``}
|
||||||
</ul>
|
</ul>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,18 @@ import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
|
||||||
import "../../../elements/forms/FormElement";
|
import "../../../elements/forms/FormElement";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import "../../FormStatic";
|
import "../../FormStatic";
|
||||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
DeviceChallenge,
|
||||||
|
} from "authentik-api";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-duo")
|
@customElement("ak-stage-authenticator-validate-duo")
|
||||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
export class AuthenticatorValidateStageWebDuo extends BaseStage<
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
|
> {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
deviceChallenge?: DeviceChallenge;
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
@ -30,25 +36,30 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
this.host?.submit({
|
this.host?.submit({
|
||||||
"duo": this.deviceChallenge?.deviceUid
|
duo: this.deviceChallenge?.deviceUid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
|
|
||||||
|
@ -61,18 +72,22 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links">
|
||||||
${this.showBackButton ?
|
${this.showBackButton
|
||||||
html`<li class="pf-c-login__main-footer-links-item">
|
? html`<li class="pf-c-login__main-footer-links-item">
|
||||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
if (!this.host) return;
|
if (!this.host) return;
|
||||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
(
|
||||||
}}>
|
this.host as AuthenticatorValidateStage
|
||||||
|
).selectedDeviceChallenge = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
${t`Return to device picker`}
|
${t`Return to device picker`}
|
||||||
</button>
|
</button>
|
||||||
</li>`:
|
</li>`
|
||||||
html``}
|
: html``}
|
||||||
</ul>
|
</ul>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,24 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import { PFSize } from "../../../elements/Spinner";
|
import { PFSize } from "../../../elements/Spinner";
|
||||||
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
|
import {
|
||||||
|
transformAssertionForServer,
|
||||||
|
transformCredentialRequestOptions,
|
||||||
|
} from "../authenticator_webauthn/utils";
|
||||||
import { BaseStage } from "../base";
|
import { BaseStage } from "../base";
|
||||||
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
|
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
|
||||||
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
|
import {
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
DeviceChallenge,
|
||||||
|
} from "authentik-api";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-webauthn")
|
@customElement("ak-stage-authenticator-validate-webauthn")
|
||||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
|
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
||||||
|
AuthenticatorValidationChallenge,
|
||||||
@property({attribute: false})
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
|
> {
|
||||||
|
@property({ attribute: false })
|
||||||
deviceChallenge?: DeviceChallenge;
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
|
@ -25,7 +34,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
||||||
@property()
|
@property()
|
||||||
authenticateMessage = "";
|
authenticateMessage = "";
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
showBackButton = false;
|
showBackButton = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -35,8 +44,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
||||||
async authenticate(): Promise<void> {
|
async authenticate(): Promise<void> {
|
||||||
// convert certain members of the PublicKeyCredentialRequestOptions into
|
// convert certain members of the PublicKeyCredentialRequestOptions into
|
||||||
// byte arrays as expected by the spec.
|
// byte arrays as expected by the spec.
|
||||||
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
|
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>(
|
||||||
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
|
this.deviceChallenge?.challenge
|
||||||
|
);
|
||||||
|
const transformedCredentialRequestOptions =
|
||||||
|
transformCredentialRequestOptions(credentialRequestOptions);
|
||||||
|
|
||||||
// request the authenticator to create an assertion signature using the
|
// request the authenticator to create an assertion signature using the
|
||||||
// credential private key
|
// credential private key
|
||||||
|
@ -54,12 +66,14 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
||||||
|
|
||||||
// we now have an authentication assertion! encode the byte arrays contained
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
// in the assertion data as strings for posting to the server
|
// 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.
|
// post the assertion to the server for verification.
|
||||||
try {
|
try {
|
||||||
await this.host?.submit({
|
await this.host?.submit({
|
||||||
webauthn: transformedAssertionForServer
|
webauthn: transformedAssertionForServer,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(t`Error when validating assertion on server: ${err}`);
|
throw new Error(t`Error when validating assertion on server: ${err}`);
|
||||||
|
@ -75,48 +89,56 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.authenticateRunning = true;
|
this.authenticateRunning = true;
|
||||||
this.authenticate().catch((e) => {
|
this.authenticate()
|
||||||
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.authenticateMessage = e.toString();
|
this.authenticateMessage = e.toString();
|
||||||
}).finally(() => {
|
})
|
||||||
|
.finally(() => {
|
||||||
this.authenticateRunning = false;
|
this.authenticateRunning = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<div class="pf-c-login__main-body">
|
||||||
${this.authenticateRunning ?
|
${this.authenticateRunning
|
||||||
html`<div class="pf-c-empty-state__content">
|
? html`<div class="pf-c-empty-state__content">
|
||||||
<div class="pf-l-bullseye">
|
<div class="pf-l-bullseye">
|
||||||
<div class="pf-l-bullseye__item">
|
<div class="pf-l-bullseye__item">
|
||||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`:
|
</div>`
|
||||||
html`
|
: html` <div class="pf-c-form__group pf-m-action">
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<p class="pf-m-block">${this.authenticateMessage}</p>
|
<p class="pf-m-block">${this.authenticateMessage}</p>
|
||||||
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-primary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
this.authenticateWrapper();
|
this.authenticateWrapper();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${t`Retry authentication`}
|
${t`Retry authentication`}
|
||||||
</button>
|
</button>
|
||||||
</div>`}
|
</div>`}
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links">
|
||||||
${this.showBackButton ?
|
${this.showBackButton
|
||||||
html`<li class="pf-c-login__main-footer-links-item">
|
? html`<li class="pf-c-login__main-footer-links-item">
|
||||||
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
if (!this.host) return;
|
if (!this.host) return;
|
||||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
(
|
||||||
}}>
|
this.host as AuthenticatorValidateStage
|
||||||
|
).selectedDeviceChallenge = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
${t`Return to device picker`}
|
${t`Return to device picker`}
|
||||||
</button>
|
</button>
|
||||||
</li>`:
|
</li>`
|
||||||
html``}
|
: html``}
|
||||||
</ul>
|
</ul>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import { PFSize } from "../../../elements/Spinner";
|
import { PFSize } from "../../../elements/Spinner";
|
||||||
import { BaseStage } from "../base";
|
import { BaseStage } from "../base";
|
||||||
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
|
import {
|
||||||
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
|
Assertion,
|
||||||
|
transformCredentialCreateOptions,
|
||||||
|
transformNewAssertionForServer,
|
||||||
|
} from "./utils";
|
||||||
|
import {
|
||||||
|
AuthenticatorWebAuthnChallenge,
|
||||||
|
AuthenticatorWebAuthnChallengeResponseRequest,
|
||||||
|
} from "authentik-api";
|
||||||
|
|
||||||
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
|
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
|
||||||
response: Assertion;
|
response: Assertion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-webauthn")
|
@customElement("ak-stage-authenticator-webauthn")
|
||||||
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
|
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
|
||||||
|
AuthenticatorWebAuthnChallenge,
|
||||||
@property({type: Boolean})
|
AuthenticatorWebAuthnChallengeResponseRequest
|
||||||
|
> {
|
||||||
|
@property({ type: Boolean })
|
||||||
registerRunning = false;
|
registerRunning = false;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
|
@ -35,13 +44,15 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
||||||
}
|
}
|
||||||
// convert certain members of the PublicKeyCredentialCreateOptions into
|
// convert certain members of the PublicKeyCredentialCreateOptions into
|
||||||
// byte arrays as expected by the spec.
|
// 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.
|
// request the authenticator(s) to create a new credential keypair.
|
||||||
let credential;
|
let credential;
|
||||||
try {
|
try {
|
||||||
credential = <PublicKeyCredential> await navigator.credentials.create({
|
credential = <PublicKeyCredential>await navigator.credentials.create({
|
||||||
publicKey: publicKeyCredentialCreateOptions
|
publicKey: publicKeyCredentialCreateOptions,
|
||||||
});
|
});
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
throw new Error("Credential is empty");
|
throw new Error("Credential is empty");
|
||||||
|
@ -58,7 +69,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
||||||
// and storing the public key
|
// and storing the public key
|
||||||
try {
|
try {
|
||||||
await this.host?.submit({
|
await this.host?.submit({
|
||||||
response: newAssertionForServer
|
response: newAssertionForServer,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(t`Server validation of credential failed: ${err}`);
|
throw new Error(t`Server validation of credential failed: ${err}`);
|
||||||
|
@ -70,10 +81,12 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.registerRunning = true;
|
this.registerRunning = true;
|
||||||
this.register().catch((e) => {
|
this.register()
|
||||||
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
this.registerMessage = e.toString();
|
this.registerMessage = e.toString();
|
||||||
}).finally(() => {
|
})
|
||||||
|
.finally(() => {
|
||||||
this.registerRunning = false;
|
this.registerRunning = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -89,26 +102,32 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
${this.registerRunning ?
|
${
|
||||||
html`<div class="pf-c-empty-state__content">
|
this.registerRunning
|
||||||
|
? html`<div class="pf-c-empty-state__content">
|
||||||
<div class="pf-l-bullseye">
|
<div class="pf-l-bullseye">
|
||||||
<div class="pf-l-bullseye__item">
|
<div class="pf-l-bullseye__item">
|
||||||
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`:
|
</div>`
|
||||||
html`
|
: html` <div class="pf-c-form__group pf-m-action">
|
||||||
<div class="pf-c-form__group pf-m-action">
|
${this.challenge?.responseErrors
|
||||||
${this.challenge?.responseErrors ?
|
? html`<p class="pf-m-block">
|
||||||
html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
|
${this.challenge.responseErrors["response"][0].string}
|
||||||
html``}
|
</p>`
|
||||||
|
: html``}
|
||||||
<p class="pf-m-block">${this.registerMessage}</p>
|
<p class="pf-m-block">${this.registerMessage}</p>
|
||||||
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
|
<button
|
||||||
|
class="pf-c-button pf-m-primary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
this.registerWrapper();
|
this.registerWrapper();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${t`Register device`}
|
${t`Register device`}
|
||||||
</button>
|
</button>
|
||||||
</div>`}
|
</div>`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
|
@ -116,5 +135,4 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
|
||||||
</ul>
|
</ul>
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,30 +2,28 @@ import * as base64js from "base64-js";
|
||||||
import { hexEncode } from "../../../utils";
|
import { hexEncode } from "../../../utils";
|
||||||
|
|
||||||
export function b64enc(buf: Uint8Array): string {
|
export function b64enc(buf: Uint8Array): string {
|
||||||
return base64js.fromByteArray(buf)
|
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=/g, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function b64RawEnc(buf: Uint8Array): string {
|
export function b64RawEnc(buf: Uint8Array): string {
|
||||||
return base64js.fromByteArray(buf)
|
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms items in the credentialCreateOptions generated on the server
|
* Transforms items in the credentialCreateOptions generated on the server
|
||||||
* into byte arrays expected by the navigator.credentials.create() call
|
* 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;
|
const user = credentialCreateOptions.user;
|
||||||
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
|
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
|
||||||
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
const challenge = u8arr(credentialCreateOptions.challenge.toString());
|
||||||
|
|
||||||
const transformedCredentialCreateOptions = Object.assign(
|
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
|
||||||
{}, credentialCreateOptions,
|
challenge,
|
||||||
{ challenge, user });
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
return transformedCredentialCreateOptions;
|
return transformedCredentialCreateOptions;
|
||||||
}
|
}
|
||||||
|
@ -46,11 +44,10 @@ export interface Assertion {
|
||||||
*/
|
*/
|
||||||
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||||
const attObj = new Uint8Array(
|
const attObj = new Uint8Array(
|
||||||
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject);
|
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject,
|
||||||
const clientDataJSON = new Uint8Array(
|
);
|
||||||
newAssertion.response.clientDataJSON);
|
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||||
const rawId = new Uint8Array(
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
newAssertion.rawId);
|
|
||||||
|
|
||||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
return {
|
return {
|
||||||
|
@ -59,26 +56,32 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
|
||||||
type: newAssertion.type,
|
type: newAssertion.type,
|
||||||
attObj: b64enc(attObj),
|
attObj: b64enc(attObj),
|
||||||
clientData: b64enc(clientDataJSON),
|
clientData: b64enc(clientDataJSON),
|
||||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
|
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function u8arr(input: string): Uint8Array {
|
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 challenge = u8arr(credentialRequestOptions.challenge.toString());
|
||||||
|
|
||||||
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(credentialDescriptor => {
|
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||||
|
(credentialDescriptor) => {
|
||||||
const id = u8arr(credentialDescriptor.id.toString());
|
const id = u8arr(credentialDescriptor.id.toString());
|
||||||
return Object.assign({}, credentialDescriptor, { id });
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const transformedCredentialRequestOptions = Object.assign(
|
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
|
||||||
{},
|
challenge,
|
||||||
credentialRequestOptions,
|
allowCredentials,
|
||||||
{ challenge, allowCredentials });
|
});
|
||||||
|
|
||||||
return transformedCredentialRequestOptions;
|
return transformedCredentialRequestOptions;
|
||||||
}
|
}
|
||||||
|
@ -97,8 +100,8 @@ export interface AuthAssertion {
|
||||||
* Encodes the binary data in the assertion into strings for posting to the server.
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
* @param {PublicKeyCredential} newAssertion
|
* @param {PublicKeyCredential} newAssertion
|
||||||
*/
|
*/
|
||||||
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion{
|
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||||
const response = <AuthenticatorAssertionResponse> newAssertion.response;
|
const response = <AuthenticatorAssertionResponse>newAssertion.response;
|
||||||
const authData = new Uint8Array(response.authenticatorData);
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
const rawId = new Uint8Array(newAssertion.rawId);
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
@ -112,6 +115,6 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
|
||||||
authData: b64RawEnc(authData),
|
authData: b64RawEnc(authData),
|
||||||
clientData: b64RawEnc(clientDataJSON),
|
clientData: b64RawEnc(clientDataJSON),
|
||||||
signature: hexEncode(sig),
|
signature: hexEncode(sig),
|
||||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
|
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,36 +12,37 @@ import "../../../elements/EmptyState";
|
||||||
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
|
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
|
||||||
|
|
||||||
@customElement("ak-stage-autosubmit")
|
@customElement("ak-stage-autosubmit")
|
||||||
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
|
export class AutosubmitStage extends BaseStage<
|
||||||
|
AutosubmitChallenge,
|
||||||
|
AutoSubmitChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(): void {
|
updated(): void {
|
||||||
this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit();});
|
this.shadowRoot?.querySelectorAll("form").forEach((form) => {
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
|
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
|
||||||
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
|
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||||
return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
|
return html`<input
|
||||||
|
type="hidden"
|
||||||
|
name="${key as string}"
|
||||||
|
value="${value as string}"
|
||||||
|
/>`;
|
||||||
})}
|
})}
|
||||||
<ak-empty-state
|
<ak-empty-state ?loading="${true}"> </ak-empty-state>
|
||||||
?loading="${true}">
|
|
||||||
</ak-empty-state>
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
${t`Continue`}
|
${t`Continue`}
|
||||||
|
@ -50,9 +51,7 @@ export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitCh
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ export interface StageHost {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseStage<Tin, Tout> extends LitElement {
|
export class BaseStage<Tin, Tout> extends LitElement {
|
||||||
|
|
||||||
host!: StageHost;
|
host!: StageHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
|
@ -19,7 +18,7 @@ export class BaseStage<Tin, Tout> extends LitElement {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
} = {};
|
} = {};
|
||||||
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
|
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);
|
this.host?.submit(object as unknown as Tout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,18 +27,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-form__alert">
|
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">
|
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
|
||||||
<div class="pf-c-alert__icon">
|
<div class="pf-c-alert__icon">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="pf-c-alert__title">
|
<h4 class="pf-c-alert__title">${err.string}</h4>
|
||||||
${err.string}
|
|
||||||
</h4>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-stage-captcha")
|
@customElement("ak-stage-captcha")
|
||||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -38,7 +37,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||||
sitekey: this.challenge.siteKey,
|
sitekey: this.challenge.siteKey,
|
||||||
callback: (token) => {
|
callback: (token) => {
|
||||||
this.host?.submit({
|
this.host?.submit({
|
||||||
"token": token,
|
token: token,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
size: "invisible",
|
size: "invisible",
|
||||||
|
@ -51,24 +50,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form">
|
<form class="pf-c-form">
|
||||||
<ak-form-static
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<div class="ak-loading">
|
<div class="ak-loading">
|
||||||
|
@ -77,9 +74,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,48 +15,63 @@ import "../../FormStatic";
|
||||||
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
|
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
|
||||||
import { ifDefined } from "lit-html/directives/if-defined";
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-stage-consent")
|
@customElement("ak-stage-consent")
|
||||||
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
|
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
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 {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</ak-form-static>
|
||||||
<div class="pf-c-form__group">
|
<div class="pf-c-form__group">
|
||||||
<p id="header-text" class="pf-u-mb-xl">
|
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
|
||||||
${this.challenge.headerText}
|
${this.challenge.permissions.length > 0
|
||||||
|
? html`
|
||||||
|
<p class="pf-u-mb-sm">
|
||||||
|
${t`Application requires following permissions:`}
|
||||||
</p>
|
</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">
|
<ul class="pf-c-list" id="permmissions">
|
||||||
${this.challenge.permissions.map((permission) => {
|
${this.challenge.permissions.map((permission) => {
|
||||||
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
|
return html`<li data-permission-code="${permission.id}">
|
||||||
|
${permission.name}
|
||||||
|
</li>`;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
` : html``}
|
`
|
||||||
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
@ -67,9 +82,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,25 +14,24 @@ import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
|
||||||
|
|
||||||
@customElement("ak-stage-dummy")
|
@customElement("ak-stage-dummy")
|
||||||
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
|
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
${t`Continue`}
|
${t`Continue`}
|
||||||
|
@ -41,9 +40,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,29 +13,26 @@ import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
|
||||||
|
|
||||||
@customElement("ak-stage-email")
|
@customElement("ak-stage-email")
|
||||||
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
|
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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">
|
<div class="pf-c-form__group">
|
||||||
<p>
|
<p>${t`Check your Emails for a password reset link.`}</p>
|
||||||
${t`Check your Emails for a password reset link.`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
@ -46,9 +43,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,12 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import "../../../elements/forms/FormElement";
|
import "../../../elements/forms/FormElement";
|
||||||
import "../../../elements/EmptyState";
|
import "../../../elements/EmptyState";
|
||||||
import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api";
|
import {
|
||||||
|
IdentificationChallenge,
|
||||||
|
IdentificationChallengeResponseRequest,
|
||||||
|
LoginSource,
|
||||||
|
UserFieldsEnum,
|
||||||
|
} from "authentik-api";
|
||||||
|
|
||||||
export const PasswordManagerPrefill: {
|
export const PasswordManagerPrefill: {
|
||||||
password: string | undefined;
|
password: string | undefined;
|
||||||
|
@ -21,13 +26,27 @@ export const PasswordManagerPrefill: {
|
||||||
totp: undefined,
|
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")
|
@customElement("ak-stage-identification")
|
||||||
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
|
export class IdentificationStage extends BaseStage<
|
||||||
|
IdentificationChallenge,
|
||||||
|
IdentificationChallengeResponseRequest
|
||||||
|
> {
|
||||||
static get styles(): CSSResult[] {
|
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`
|
css`
|
||||||
/* login page's icons */
|
/* login page's icons */
|
||||||
.pf-c-login__main-footer-links-item button {
|
.pf-c-login__main-footer-links-item button {
|
||||||
|
@ -41,7 +60,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +75,9 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
username.setAttribute("autocomplete", "username");
|
username.setAttribute("autocomplete", "username");
|
||||||
username.onkeyup = (ev: Event) => {
|
username.onkeyup = (ev: Event) => {
|
||||||
const el = ev.target as HTMLInputElement;
|
const el = ev.target as HTMLInputElement;
|
||||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
(this.shadowRoot || this)
|
||||||
|
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||||
|
.forEach((input) => {
|
||||||
input.value = el.value;
|
input.value = el.value;
|
||||||
// Because we assume only one input field exists that matches this
|
// Because we assume only one input field exists that matches this
|
||||||
// call focus so the user can press enter
|
// call focus so the user can press enter
|
||||||
|
@ -79,7 +100,9 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
PasswordManagerPrefill.password = el.value;
|
PasswordManagerPrefill.password = el.value;
|
||||||
// Because password managers fill username, then password,
|
// Because password managers fill username, then password,
|
||||||
// we need to re-focus the uid_field here too
|
// we need to re-focus the uid_field here too
|
||||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
(this.shadowRoot || this)
|
||||||
|
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||||
|
.forEach((input) => {
|
||||||
// Because we assume only one input field exists that matches this
|
// Because we assume only one input field exists that matches this
|
||||||
// call focus so the user can press enter
|
// call focus so the user can press enter
|
||||||
input.focus();
|
input.focus();
|
||||||
|
@ -101,7 +124,9 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
PasswordManagerPrefill.totp = el.value;
|
PasswordManagerPrefill.totp = el.value;
|
||||||
// Because totp managers fill username, then password, then optionally,
|
// Because totp managers fill username, then password, then optionally,
|
||||||
// we need to re-focus the uid_field here too
|
// we need to re-focus the uid_field here too
|
||||||
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
|
(this.shadowRoot || this)
|
||||||
|
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
|
||||||
|
.forEach((input) => {
|
||||||
// Because we assume only one input field exists that matches this
|
// Because we assume only one input field exists that matches this
|
||||||
// call focus so the user can press enter
|
// call focus so the user can press enter
|
||||||
input.focus();
|
input.focus();
|
||||||
|
@ -113,13 +138,16 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
renderSource(source: LoginSource): TemplateResult {
|
renderSource(source: LoginSource): TemplateResult {
|
||||||
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
|
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
|
||||||
if (source.iconUrl) {
|
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">
|
return html`<li class="pf-c-login__main-footer-links-item">
|
||||||
<button type="button" @click=${() => {
|
<button
|
||||||
|
type="button"
|
||||||
|
@click=${() => {
|
||||||
if (!this.host) return;
|
if (!this.host) return;
|
||||||
this.host.challenge = source.challenge;
|
this.host.challenge = source.challenge;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
${icon}
|
${icon}
|
||||||
</button>
|
</button>
|
||||||
</li>`;
|
</li>`;
|
||||||
|
@ -130,24 +158,26 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-login__main-footer-band">
|
return html`<div class="pf-c-login__main-footer-band">
|
||||||
${this.challenge.enrollUrl ? html`
|
${this.challenge.enrollUrl
|
||||||
<p class="pf-c-login__main-footer-band-item">
|
? html` <p class="pf-c-login__main-footer-band-item">
|
||||||
${t`Need an account?`}
|
${t`Need an account?`}
|
||||||
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
|
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
|
||||||
</p>` : html``}
|
</p>`
|
||||||
${this.challenge.recoveryUrl ? html`
|
: html``}
|
||||||
<p class="pf-c-login__main-footer-band-item">
|
${this.challenge.recoveryUrl
|
||||||
<a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
|
? html` <p class="pf-c-login__main-footer-band-item">
|
||||||
</p>` : html``}
|
<a id="recovery" href="${this.challenge.recoveryUrl}"
|
||||||
|
>${t`Forgot username or password?`}</a
|
||||||
|
>
|
||||||
|
</p>`
|
||||||
|
: html``}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInput(): TemplateResult {
|
renderInput(): TemplateResult {
|
||||||
let type = "text";
|
let type = "text";
|
||||||
if (!this.challenge?.userFields) {
|
if (!this.challenge?.userFields) {
|
||||||
return html`<p>
|
return html`<p>${t`Select one of the sources below to login.`}</p>`;
|
||||||
${t`Select one of the sources below to login.`}
|
|
||||||
</p>`;
|
|
||||||
}
|
}
|
||||||
const fields = (this.challenge?.userFields || []).sort();
|
const fields = (this.challenge?.userFields || []).sort();
|
||||||
// Check if the field should be *only* email to set the input type
|
// 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.Email]: t`Email`,
|
||||||
[UserFieldsEnum.Upn]: t`UPN`,
|
[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
|
return html`<ak-form-element
|
||||||
label=${label}
|
label=${label}
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge.responseErrors || {})["uid_field"]}>
|
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
|
||||||
|
>
|
||||||
<!-- @ts-ignore -->
|
<!-- @ts-ignore -->
|
||||||
<input type=${type}
|
<input
|
||||||
|
type=${type}
|
||||||
name="uidField"
|
name="uidField"
|
||||||
placeholder=${label}
|
placeholder=${label}
|
||||||
autofocus=""
|
autofocus=""
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required>
|
required
|
||||||
|
/>
|
||||||
</ak-form-element>
|
</ak-form-element>
|
||||||
${this.challenge.passwordFields ? html`
|
${this.challenge.passwordFields
|
||||||
|
? html`
|
||||||
<ak-form-element
|
<ak-form-element
|
||||||
label="${t`Password`}"
|
label="${t`Password`}"
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge.responseErrors || {})["password"]}>
|
.errors=${(this.challenge.responseErrors || {})["password"]}
|
||||||
<input type="password"
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="${t`Password`}"
|
placeholder="${t`Password`}"
|
||||||
autofocus=""
|
autofocus=""
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
value=${PasswordManagerPrefill.password || ""}>
|
value=${PasswordManagerPrefill.password || ""}
|
||||||
|
/>
|
||||||
</ak-form-element>
|
</ak-form-element>
|
||||||
`: html``}
|
`
|
||||||
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
|
: html``}
|
||||||
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
|
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||||
html``}
|
? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
|
||||||
|
: html``}
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
${this.challenge.primaryAction}
|
${this.challenge.primaryAction}
|
||||||
|
@ -202,23 +240,21 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
|
<form
|
||||||
${this.challenge.applicationPre ?
|
class="pf-c-form"
|
||||||
html`<p>
|
@submit=${(e: Event) => {
|
||||||
${t`Login to continue to ${this.challenge.applicationPre}.`}
|
this.submitForm(e);
|
||||||
</p>`:
|
}}
|
||||||
html``}
|
>
|
||||||
|
${this.challenge.applicationPre
|
||||||
|
? html`<p>${t`Login to continue to ${this.challenge.applicationPre}.`}</p>`
|
||||||
|
: html``}
|
||||||
${this.renderInput()}
|
${this.renderInput()}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,5 +267,4 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
|
||||||
${this.renderFooter()}
|
${this.renderFooter()}
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,52 +17,62 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-stage-password")
|
@customElement("ak-stage-password")
|
||||||
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
|
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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
|
<ak-form-static
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
user=${this.challenge.pendingUser}>
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
<div slot="link">
|
<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>
|
</div>
|
||||||
</ak-form-static>
|
</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
|
<ak-form-element
|
||||||
label="${t`Password`}"
|
label="${t`Password`}"
|
||||||
?required="${true}"
|
?required="${true}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})["password"]}>
|
.errors=${(this.challenge?.responseErrors || {})["password"]}
|
||||||
<input type="password"
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="${t`Please enter your password`}"
|
placeholder="${t`Please enter your password`}"
|
||||||
autofocus=""
|
autofocus=""
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
required
|
||||||
value=${PasswordManagerPrefill.password || ""}>
|
value=${PasswordManagerPrefill.password || ""}
|
||||||
|
/>
|
||||||
</ak-form-element>
|
</ak-form-element>
|
||||||
|
|
||||||
${this.challenge.recoveryUrl ?
|
${this.challenge.recoveryUrl
|
||||||
html`<a href="${this.challenge.recoveryUrl}">
|
? html`<a href="${this.challenge.recoveryUrl}"> ${t`Forgot password?`}</a>`
|
||||||
${t`Forgot password?`}</a>` : ""}
|
: ""}
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,8 @@ import "../../../elements/EmptyState";
|
||||||
import "../../../elements/Divider";
|
import "../../../elements/Divider";
|
||||||
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
|
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-stage-prompt")
|
@customElement("ak-stage-prompt")
|
||||||
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
|
||||||
}
|
}
|
||||||
|
@ -104,34 +102,41 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
if (!this.challenge) {
|
if (!this.challenge) {
|
||||||
return html`<ak-empty-state
|
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||||
?loading="${true}"
|
|
||||||
header=${t`Loading`}>
|
|
||||||
</ak-empty-state>`;
|
|
||||||
}
|
}
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
${this.challenge.flowInfo?.title}
|
|
||||||
</h1>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="pf-c-login__main-body">
|
<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) => {
|
${this.challenge.fields.map((prompt) => {
|
||||||
// Special types that aren't rendered in a wrapper
|
// 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 unsafeHTML(this.renderPromptInner(prompt));
|
||||||
}
|
}
|
||||||
return html`<ak-form-element
|
return html`<ak-form-element
|
||||||
label="${prompt.label}"
|
label="${prompt.label}"
|
||||||
?required="${prompt.required}"
|
?required="${prompt.required}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
|
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||||
|
>
|
||||||
${unsafeHTML(this.renderPromptInner(prompt))}
|
${unsafeHTML(this.renderPromptInner(prompt))}
|
||||||
</ak-form-element>`;
|
</ak-form-element>`;
|
||||||
})}
|
})}
|
||||||
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
|
${"non_field_errors" in (this.challenge?.responseErrors || {})
|
||||||
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
|
? this.renderNonFieldErrors(
|
||||||
html``}
|
this.challenge?.responseErrors?.non_field_errors || [],
|
||||||
|
)
|
||||||
|
: html``}
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
${t`Continue`}
|
${t`Continue`}
|
||||||
|
@ -140,9 +145,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
<ul class="pf-c-login__main-footer-links">
|
<ul class="pf-c-login__main-footer-links"></ul>
|
||||||
</ul>
|
|
||||||
</footer>`;
|
</footer>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import "../elements/messages/MessageContainer";
|
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 { me } from "../api/Users";
|
||||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
|
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
|
||||||
import "./locale";
|
import "./locale";
|
||||||
|
@ -21,24 +29,31 @@ import { AdminApi } from "authentik-api";
|
||||||
import { DEFAULT_CONFIG } from "../api/Config";
|
import { DEFAULT_CONFIG } from "../api/Config";
|
||||||
import { WebsocketClient } from "../common/ws";
|
import { WebsocketClient } from "../common/ws";
|
||||||
|
|
||||||
|
|
||||||
@customElement("ak-interface-admin")
|
@customElement("ak-interface-admin")
|
||||||
export class AdminInterface extends LitElement {
|
export class AdminInterface extends LitElement {
|
||||||
|
@property({ type: Boolean })
|
||||||
@property({type: Boolean})
|
|
||||||
sidebarOpen = true;
|
sidebarOpen = true;
|
||||||
|
|
||||||
@property({type: Boolean})
|
@property({ type: Boolean })
|
||||||
notificationOpen = false;
|
notificationOpen = false;
|
||||||
|
|
||||||
ws: WebsocketClient;
|
ws: WebsocketClient;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFPage, PFButton, PFDrawer, AKGlobal, css`
|
return [
|
||||||
.pf-c-page__main, .pf-c-drawer__content, .pf-c-page__drawer {
|
PFBase,
|
||||||
|
PFPage,
|
||||||
|
PFButton,
|
||||||
|
PFDrawer,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
|
.pf-c-page__main,
|
||||||
|
.pf-c-drawer__content,
|
||||||
|
.pf-c-page__drawer {
|
||||||
z-index: auto !important;
|
z-index: auto !important;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -58,18 +73,29 @@ export class AdminInterface extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html` <div class="pf-c-page">
|
||||||
<div class="pf-c-page">
|
<ak-sidebar
|
||||||
<ak-sidebar class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
|
class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}"
|
||||||
|
>
|
||||||
${this.renderSidebarItems()}
|
${this.renderSidebarItems()}
|
||||||
</ak-sidebar>
|
</ak-sidebar>
|
||||||
<div class="pf-c-page__drawer">
|
<div class="pf-c-page__drawer">
|
||||||
<div class="pf-c-drawer ${this.notificationOpen ? "pf-m-expanded" : "pf-m-collapsed"}">
|
<div
|
||||||
|
class="pf-c-drawer ${this.notificationOpen
|
||||||
|
? "pf-m-expanded"
|
||||||
|
: "pf-m-collapsed"}"
|
||||||
|
>
|
||||||
<div class="pf-c-drawer__main">
|
<div class="pf-c-drawer__main">
|
||||||
<div class="pf-c-drawer__content">
|
<div class="pf-c-drawer__content">
|
||||||
<div class="pf-c-drawer__body">
|
<div class="pf-c-drawer__body">
|
||||||
<main class="pf-c-page__main">
|
<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
|
||||||
|
role="main"
|
||||||
|
class="pf-c-page__main"
|
||||||
|
tabindex="-1"
|
||||||
|
id="main-content"
|
||||||
|
defaultUrl="/library"
|
||||||
|
>
|
||||||
</ak-router-outlet>
|
</ak-router-outlet>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,30 +110,41 @@ export class AdminInterface extends LitElement {
|
||||||
|
|
||||||
renderSidebarItems(): TemplateResult {
|
renderSidebarItems(): TemplateResult {
|
||||||
const superUserCondition = () => {
|
const superUserCondition = () => {
|
||||||
return me().then(u => u.user.isSuperuser || false);
|
return me().then((u) => u.user.isSuperuser || false);
|
||||||
};
|
};
|
||||||
return html`
|
return html`
|
||||||
${until(new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then(version => {
|
${until(
|
||||||
|
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
|
||||||
if (version.versionCurrent !== VERSION) {
|
if (version.versionCurrent !== VERSION) {
|
||||||
return html`<ak-sidebar-item ?highlight=${true}>
|
return html`<ak-sidebar-item ?highlight=${true}>
|
||||||
<span slot="label">${t`A newer version of the frontend is available.`}</span>
|
<span slot="label"
|
||||||
|
>${t`A newer version of the frontend is available.`}</span
|
||||||
|
>
|
||||||
</ak-sidebar-item>`;
|
</ak-sidebar-item>`;
|
||||||
}
|
}
|
||||||
return html``;
|
return html``;
|
||||||
}))}
|
}),
|
||||||
${until(me().then((u) => {
|
)}
|
||||||
|
${until(
|
||||||
|
me().then((u) => {
|
||||||
if (u.original) {
|
if (u.original) {
|
||||||
return html`<ak-sidebar-item ?highlight=${true} ?isAbsoluteLink=${true} path=${`/-/impersonation/end/?back=${window.location.pathname}%23${window.location.hash}`}>
|
return html`<ak-sidebar-item
|
||||||
<span slot="label">${t`You're currently impersonating ${u.user.username}. Click to stop.`}</span>
|
?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>`;
|
</ak-sidebar-item>`;
|
||||||
}
|
}
|
||||||
return html``;
|
return html``;
|
||||||
}))}
|
}),
|
||||||
|
)}
|
||||||
<ak-sidebar-item path="/library">
|
<ak-sidebar-item path="/library">
|
||||||
<span slot="label">${t`Library`}</span>
|
<span slot="label">${t`Library`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Monitor`}</span>
|
<span slot="label">${t`Monitor`}</span>
|
||||||
<ak-sidebar-item path="/administration/overview">
|
<ak-sidebar-item path="/administration/overview">
|
||||||
<span slot="label">${t`Overview`}</span>
|
<span slot="label">${t`Overview`}</span>
|
||||||
|
@ -116,24 +153,31 @@ export class AdminInterface extends LitElement {
|
||||||
<span slot="label">${t`System Tasks`}</span>
|
<span slot="label">${t`System Tasks`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Resources`}</span>
|
<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>
|
<span slot="label">${t`Applications`}</span>
|
||||||
</ak-sidebar-item>
|
</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>
|
<span slot="label">${t`Sources`}</span>
|
||||||
</ak-sidebar-item>
|
</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>
|
<span slot="label">${t`Providers`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item path="/core/tenants">
|
<ak-sidebar-item path="/core/tenants">
|
||||||
<span slot="label">${t`Tenants`}</span>
|
<span slot="label">${t`Tenants`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Outposts`}</span>
|
<span slot="label">${t`Outposts`}</span>
|
||||||
<ak-sidebar-item path="/outpost/outposts">
|
<ak-sidebar-item path="/outpost/outposts">
|
||||||
<span slot="label">${t`Outposts`}</span>
|
<span slot="label">${t`Outposts`}</span>
|
||||||
|
@ -142,10 +186,12 @@ export class AdminInterface extends LitElement {
|
||||||
<span slot="label">${t`Service Connections`}</span>
|
<span slot="label">${t`Service Connections`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Events`}</span>
|
<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>
|
<span slot="label">${t`Logs`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item path="/events/rules">
|
<ak-sidebar-item path="/events/rules">
|
||||||
|
@ -155,8 +201,7 @@ export class AdminInterface extends LitElement {
|
||||||
<span slot="label">${t`Notification Transports`}</span>
|
<span slot="label">${t`Notification Transports`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Customisation`}</span>
|
<span slot="label">${t`Customisation`}</span>
|
||||||
<ak-sidebar-item path="/policy/policies">
|
<ak-sidebar-item path="/policy/policies">
|
||||||
<span slot="label">${t`Policies`}</span>
|
<span slot="label">${t`Policies`}</span>
|
||||||
|
@ -171,10 +216,12 @@ export class AdminInterface extends LitElement {
|
||||||
<span slot="label">${t`Property Mappings`}</span>
|
<span slot="label">${t`Property Mappings`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Flows`}</span>
|
<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>
|
<span slot="label">${t`Flows`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item path="/flow/stages">
|
<ak-sidebar-item path="/flow/stages">
|
||||||
|
@ -187,10 +234,12 @@ export class AdminInterface extends LitElement {
|
||||||
<span slot="label">${t`Invitations`}</span>
|
<span slot="label">${t`Invitations`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item
|
<ak-sidebar-item .condition=${superUserCondition}>
|
||||||
.condition=${superUserCondition}>
|
|
||||||
<span slot="label">${t`Identity & Cryptography`}</span>
|
<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>
|
<span slot="label">${t`Users`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item path="/identity/groups">
|
<ak-sidebar-item path="/identity/groups">
|
||||||
|
@ -205,5 +254,4 @@ export class AdminInterface extends LitElement {
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<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/patternfly-base.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.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/empty-state.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
|
||||||
<script src="/static/dist/poly.js" type="module"></script>
|
<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>
|
<script src="/static/dist/AdminInterface.js" type="module"></script>
|
||||||
<title>authentik</title>
|
<title>authentik</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -17,9 +19,13 @@
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-interface-admin>
|
<ak-interface-admin>
|
||||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<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">
|
<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__clipper"></span>
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<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/patternfly-base.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/page.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/empty-state.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css">
|
<link rel="stylesheet" type="text/css" href="/static/dist/spinner.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css">
|
<link rel="stylesheet" type="text/css" href="/static/dist/authentik.css" />
|
||||||
<script>ShadyDOM = { force: !navigator.webdriver }; window["polymerSkipLoadingFontRoboto"] = true;</script>
|
<script>
|
||||||
|
ShadyDOM = { force: !navigator.webdriver };
|
||||||
|
window["polymerSkipLoadingFontRoboto"] = true;
|
||||||
|
</script>
|
||||||
<script src="/static/dist/poly.js" type="module"></script>
|
<script src="/static/dist/poly.js" type="module"></script>
|
||||||
<script src="/static/dist/FlowInterface.js" type="module"></script>
|
<script src="/static/dist/FlowInterface.js" type="module"></script>
|
||||||
<title>authentik</title>
|
<title>authentik</title>
|
||||||
|
@ -17,9 +20,13 @@
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-flow-executor>
|
<ak-flow-executor>
|
||||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<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">
|
<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__clipper"></span>
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
import { t } from "@lingui/macro";
|
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 { ifDefined } from "lit-html/directives/if-defined";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
import { Application, CoreApi } from "authentik-api";
|
import { Application, CoreApi } from "authentik-api";
|
||||||
|
@ -20,11 +28,15 @@ import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
|
||||||
|
|
||||||
@customElement("ak-library-app")
|
@customElement("ak-library-app")
|
||||||
export class LibraryApplication extends LitElement {
|
export class LibraryApplication extends LitElement {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
application?: Application;
|
application?: Application;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFCard, PFAvatar, AKGlobal,
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFCard,
|
||||||
|
PFAvatar,
|
||||||
|
AKGlobal,
|
||||||
css`
|
css`
|
||||||
a {
|
a {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -48,7 +60,7 @@ export class LibraryApplication extends LitElement {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,19 +68,28 @@ export class LibraryApplication extends LitElement {
|
||||||
if (!this.application) {
|
if (!this.application) {
|
||||||
return html`<ak-spinner></ak-spinner>`;
|
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">
|
<div class="pf-c-card__header">
|
||||||
${this.application.metaIcon
|
${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>`}
|
: html`<i class="fas fas fa-share-square"></i>`}
|
||||||
${until(me().then((u) => {
|
${until(
|
||||||
|
me().then((u) => {
|
||||||
if (!u.user.isSuperuser) return html``;
|
if (!u.user.isSuperuser) return html``;
|
||||||
return html`
|
return html`
|
||||||
<a href="#/core/applications/${this.application?.slug}">
|
<a href="#/core/applications/${this.application?.slug}">
|
||||||
<i class="fas fa-pencil-alt"></i>
|
<i class="fas fa-pencil-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}))}
|
}),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__title">
|
<div class="pf-c-card__title">
|
||||||
<p id="card-1-check-label">${this.application.name}</p>
|
<p id="card-1-check-label">${this.application.name}</p>
|
||||||
|
@ -76,15 +97,13 @@ export class LibraryApplication extends LitElement {
|
||||||
<small>${this.application.metaPublisher}</small>
|
<small>${this.application.metaPublisher}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__body">${truncate(this.application.metaDescription, 35)}</div>
|
|
||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-library")
|
@customElement("ak-library")
|
||||||
export class LibraryPage extends LitElement {
|
export class LibraryPage extends LitElement {
|
||||||
@property({attribute: false})
|
@property({ attribute: false })
|
||||||
apps?: AKResponse<Application>;
|
apps?: AKResponse<Application>;
|
||||||
|
|
||||||
pageTitle(): string {
|
pageTitle(): string {
|
||||||
|
@ -120,20 +139,23 @@ export class LibraryPage extends LitElement {
|
||||||
|
|
||||||
renderApps(): TemplateResult {
|
renderApps(): TemplateResult {
|
||||||
return html`<div class="pf-l-gallery pf-m-gutter">
|
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>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
||||||
<ak-page-header
|
<ak-page-header icon="pf-icon pf-icon-applications" header=${t`Applications`}>
|
||||||
icon="pf-icon pf-icon-applications"
|
|
||||||
header=${t`Applications`}>
|
|
||||||
</ak-page-header>
|
</ak-page-header>
|
||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
${loading(this.apps, html`${(this.apps?.results.length || 0) > 0 ?
|
${loading(
|
||||||
this.renderApps() :
|
this.apps,
|
||||||
this.renderEmptyState()}`)}
|
html`${(this.apps?.results.length || 0) > 0
|
||||||
|
? this.renderApps()
|
||||||
|
: this.renderEmptyState()}`,
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>`;
|
</main>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,13 @@ import "../../elements/PageHeader";
|
||||||
|
|
||||||
@customElement("ak-admin-overview")
|
@customElement("ak-admin-overview")
|
||||||
export class AdminOverviewPage extends LitElement {
|
export class AdminOverviewPage extends LitElement {
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFGrid, PFPage, PFContent, AKGlobal, css`
|
return [
|
||||||
|
PFGrid,
|
||||||
|
PFPage,
|
||||||
|
PFContent,
|
||||||
|
AKGlobal,
|
||||||
|
css`
|
||||||
.row-divider {
|
.row-divider {
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
margin-bottom: -4px;
|
margin-bottom: -4px;
|
||||||
|
@ -42,85 +46,153 @@ export class AdminOverviewPage extends LitElement {
|
||||||
.card-container {
|
.card-container {
|
||||||
max-height: 10em;
|
max-height: 10em;
|
||||||
}
|
}
|
||||||
`];
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html` <ak-page-header
|
||||||
<ak-page-header
|
|
||||||
icon=""
|
icon=""
|
||||||
header=${t`System Overview`}
|
header=${t`System Overview`}
|
||||||
description=${t`General system status`}>
|
description=${t`General system status`}
|
||||||
|
>
|
||||||
</ak-page-header>
|
</ak-page-header>
|
||||||
<section class="pf-c-page__main-section">
|
<section class="pf-c-page__main-section">
|
||||||
<div class="pf-l-grid pf-m-gutter">
|
<div class="pf-l-grid pf-m-gutter">
|
||||||
<!-- row 1 -->
|
<!-- 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">
|
<div
|
||||||
<ak-aggregate-card icon="pf-icon pf-icon-infrastructure" header=${t`Policies`} headerLink="#/policy/policies">
|
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-admin-status-chart-policy></ak-admin-status-chart-policy>
|
||||||
</ak-aggregate-card>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Flows`} headerLink="#/flow/flows">
|
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-admin-status-chart-flow></ak-admin-status-chart-flow>
|
||||||
</ak-aggregate-card>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Outpost status`} headerLink="#/outpost/outposts">
|
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-admin-status-chart-outpost></ak-admin-status-chart-outpost>
|
||||||
</ak-aggregate-card>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Users`} headerLink="#/identity/users">
|
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-admin-status-chart-user-count></ak-admin-status-chart-user-count>
|
||||||
</ak-aggregate-card>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`Groups`} headerLink="#/identity/groups">
|
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-admin-status-chart-group-count></ak-admin-status-chart-group-count>
|
||||||
</ak-aggregate-card>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="fa fa-sync-alt" header=${t`LDAP Sync status`} headerLink="#/core/sources">
|
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-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<!-- row 2 -->
|
<!-- 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">
|
<div
|
||||||
<ak-admin-status-version icon="pf-icon pf-icon-bundle" header=${t`Version`} headerLink="https://github.com/goauthentik/authentik/releases">
|
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>
|
</ak-admin-status-version>
|
||||||
</div>
|
</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">
|
<div
|
||||||
<ak-admin-status-card-backup icon="fa fa-database" header=${t`Backup status`} headerLink="#/administration/system-tasks">
|
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>
|
</ak-admin-status-card-backup>
|
||||||
</div>
|
</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">
|
<div
|
||||||
<ak-admin-status-card-workers icon="pf-icon pf-icon-server" header=${t`Workers`}>
|
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>
|
</ak-admin-status-card-workers>
|
||||||
</div>
|
</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">
|
<div
|
||||||
<ak-admin-status-system icon="pf-icon pf-icon-server" header=${t`System status`}>
|
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>
|
</ak-admin-status-system>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
<!-- row 3 -->
|
<!-- 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">
|
<div
|
||||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Logins over the last 24 hours`}>
|
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-charts-admin-login></ak-charts-admin-login>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
</div>
|
</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">
|
<div
|
||||||
<ak-aggregate-card icon="pf-icon pf-icon-server" header=${t`Apps with most usage`}>
|
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-top-applications-table></ak-top-applications-table>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,7 @@ import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
|
|
||||||
@customElement("ak-top-applications-table")
|
@customElement("ak-top-applications-table")
|
||||||
export class TopApplicationsTable extends LitElement {
|
export class TopApplicationsTable extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
@property({attribute: false})
|
|
||||||
topN?: EventTopPerUser[];
|
topN?: EventTopPerUser[];
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -18,24 +17,25 @@ export class TopApplicationsTable extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
new EventsApi(DEFAULT_CONFIG).eventsEventsTopPerUserList({
|
new EventsApi(DEFAULT_CONFIG)
|
||||||
|
.eventsEventsTopPerUserList({
|
||||||
action: "authorize_application",
|
action: "authorize_application",
|
||||||
topN: 11,
|
topN: 11,
|
||||||
}).then((events) => {
|
})
|
||||||
|
.then((events) => {
|
||||||
this.topN = events;
|
this.topN = events;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRow(event: EventTopPerUser): TemplateResult {
|
renderRow(event: EventTopPerUser): TemplateResult {
|
||||||
return html`<tr role="row">
|
return html`<tr role="row">
|
||||||
|
<td role="cell">${event.application.name}</td>
|
||||||
|
<td role="cell">${event.countedEvents}</td>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
${event.application.name}
|
<progress
|
||||||
</td>
|
value="${event.countedEvents}"
|
||||||
<td role="cell">
|
max="${this.topN ? this.topN[0].countedEvents : 0}"
|
||||||
${event.countedEvents}
|
></progress>
|
||||||
</td>
|
|
||||||
<td role="cell">
|
|
||||||
<progress value="${event.countedEvents}" max="${this.topN ? this.topN[0].countedEvents : 0}"></progress>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,10 @@ export class TopApplicationsTable extends LitElement {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody role="rowgroup">
|
<tbody role="rowgroup">
|
||||||
${this.topN ? this.topN.map((e) => this.renderRow(e)) : html`<ak-spinner></ak-spinner>`}
|
${this.topN
|
||||||
|
? this.topN.map((e) => this.renderRow(e))
|
||||||
|
: html`<ak-spinner></ak-spinner>`}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ export interface AdminStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AdminStatusCard<T> extends AggregateCard {
|
export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||||
|
|
||||||
abstract getPrimaryValue(): Promise<T>;
|
abstract getPrimaryValue(): Promise<T>;
|
||||||
|
|
||||||
abstract getStatus(value: T): Promise<AdminStatus>;
|
abstract getStatus(value: T): Promise<AdminStatus>;
|
||||||
|
@ -30,16 +29,20 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
|
||||||
|
|
||||||
renderInner(): TemplateResult {
|
renderInner(): TemplateResult {
|
||||||
return html`<p class="center-value">
|
return html`<p class="center-value">
|
||||||
${until(this.getPrimaryValue().then((v) => {
|
${until(
|
||||||
|
this.getPrimaryValue()
|
||||||
|
.then((v) => {
|
||||||
this.value = v;
|
this.value = v;
|
||||||
return this.getStatus(v);
|
return this.getStatus(v);
|
||||||
}).then((status) => {
|
})
|
||||||
return html`<p>
|
.then((status) => {
|
||||||
<i class="${status.icon}"></i> ${this.renderValue()}
|
return html`<p><i class="${status.icon}"></i> ${this.renderValue()}</p>
|
||||||
</p>
|
${status.message
|
||||||
${status.message ? html`<p class="subtext">${status.message}</p>` : html``}`;
|
? html`<p class="subtext">${status.message}</p>`
|
||||||
}), html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`)}
|
: html``}`;
|
||||||
|
}),
|
||||||
|
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
|
||||||
|
)}
|
||||||
</p>`;
|
</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,18 @@ import { convertToTitle } from "../../../utils";
|
||||||
|
|
||||||
@customElement("ak-admin-status-card-backup")
|
@customElement("ak-admin-status-card-backup")
|
||||||
export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
|
export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
|
||||||
|
|
||||||
getPrimaryValue(): Promise<StatusEnum> {
|
getPrimaryValue(): Promise<StatusEnum> {
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminSystemTasksRetrieve({
|
return new AdminApi(DEFAULT_CONFIG)
|
||||||
id: "backup_database"
|
.adminSystemTasksRetrieve({
|
||||||
}).then((value) => {
|
id: "backup_database",
|
||||||
|
})
|
||||||
|
.then((value) => {
|
||||||
return value.status;
|
return value.status;
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// On error (probably 404), check the config and see if the server
|
// On error (probably 404), check the config and see if the server
|
||||||
// can even backup
|
// can even backup
|
||||||
return config().then(c => {
|
return config().then((c) => {
|
||||||
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
|
if (c.capabilities.includes(CapabilitiesEnum.Backup)) {
|
||||||
return StatusEnum.Error;
|
return StatusEnum.Error;
|
||||||
}
|
}
|
||||||
|
@ -43,9 +45,8 @@ export class BackupStatusCard extends AdminStatusCard<StatusEnum> {
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
return Promise.resolve<AdminStatus>({
|
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")
|
@customElement("ak-admin-status-system")
|
||||||
export class SystemStatusCard extends AdminStatusCard<System> {
|
export class SystemStatusCard extends AdminStatusCard<System> {
|
||||||
|
|
||||||
now?: Date;
|
now?: Date;
|
||||||
|
|
||||||
header = "OK";
|
header = "OK";
|
||||||
|
@ -35,12 +34,11 @@ export class SystemStatusCard extends AdminStatusCard<System> {
|
||||||
}
|
}
|
||||||
return Promise.resolve<AdminStatus>({
|
return Promise.resolve<AdminStatus>({
|
||||||
icon: "fa fa-check-circle pf-m-success",
|
icon: "fa fa-check-circle pf-m-success",
|
||||||
message: t`Everything is ok.`
|
message: t`Everything is ok.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValue(): TemplateResult {
|
renderValue(): TemplateResult {
|
||||||
return html`${this.header}`;
|
return html`${this.header}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
|
||||||
|
|
||||||
@customElement("ak-admin-status-version")
|
@customElement("ak-admin-status-version")
|
||||||
export class VersionStatusCard extends AdminStatusCard<Version> {
|
export class VersionStatusCard extends AdminStatusCard<Version> {
|
||||||
|
|
||||||
getPrimaryValue(): Promise<Version> {
|
getPrimaryValue(): Promise<Version> {
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
return new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
||||||
}
|
}
|
||||||
|
@ -26,12 +25,11 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
|
||||||
}
|
}
|
||||||
return Promise.resolve<AdminStatus>({
|
return Promise.resolve<AdminStatus>({
|
||||||
icon: "fa fa-check-circle pf-m-success",
|
icon: "fa fa-check-circle pf-m-success",
|
||||||
message: t`Up-to-date!`
|
message: t`Up-to-date!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderValue(): TemplateResult {
|
renderValue(): TemplateResult {
|
||||||
return html`${this.value?.versionCurrent}`;
|
return html`${this.value?.versionCurrent}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
|
||||||
|
|
||||||
@customElement("ak-admin-status-card-workers")
|
@customElement("ak-admin-status-card-workers")
|
||||||
export class WorkersStatusCard extends AdminStatusCard<number> {
|
export class WorkersStatusCard extends AdminStatusCard<number> {
|
||||||
|
|
||||||
getPrimaryValue(): Promise<number> {
|
getPrimaryValue(): Promise<number> {
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminWorkersRetrieve().then((workers) => {
|
return new AdminApi(DEFAULT_CONFIG).adminWorkersRetrieve().then((workers) => {
|
||||||
return workers.count;
|
return workers.count;
|
||||||
|
@ -21,9 +20,8 @@ export class WorkersStatusCard extends AdminStatusCard<number> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve<AdminStatus>({
|
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")
|
@customElement("ak-admin-status-chart-flow")
|
||||||
export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
||||||
|
|
||||||
getChartType(): string {
|
getChartType(): string {
|
||||||
return "doughnut";
|
return "doughnut";
|
||||||
}
|
}
|
||||||
|
@ -32,9 +31,11 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
||||||
async apiRequest(): Promise<FlowMetrics> {
|
async apiRequest(): Promise<FlowMetrics> {
|
||||||
const api = new FlowsApi(DEFAULT_CONFIG);
|
const api = new FlowsApi(DEFAULT_CONFIG);
|
||||||
const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0;
|
const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0;
|
||||||
const count = (await api.flowsInstancesList({
|
const count = (
|
||||||
pageSize: 1
|
await api.flowsInstancesList({
|
||||||
})).pagination.count;
|
pageSize: 1,
|
||||||
|
})
|
||||||
|
).pagination.count;
|
||||||
this.centerText = count.toString();
|
this.centerText = count.toString();
|
||||||
return {
|
return {
|
||||||
count: count - cached,
|
count: count - cached,
|
||||||
|
@ -44,24 +45,14 @@ export class PolicyStatusChart extends AKChart<FlowMetrics> {
|
||||||
|
|
||||||
getChartData(data: FlowMetrics): ChartData {
|
getChartData(data: FlowMetrics): ChartData {
|
||||||
return {
|
return {
|
||||||
labels: [
|
labels: [t`Total flows`, t`Cached flows`],
|
||||||
t`Total flows`,
|
|
||||||
t`Cached flows`,
|
|
||||||
],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: [
|
backgroundColor: ["#2b9af3", "#3e8635"],
|
||||||
"#2b9af3",
|
|
||||||
"#3e8635",
|
|
||||||
],
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: [
|
data: [data.count, data.cached],
|
||||||
data.count,
|
|
||||||
data.cached,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ interface GroupMetrics {
|
||||||
|
|
||||||
@customElement("ak-admin-status-chart-group-count")
|
@customElement("ak-admin-status-chart-group-count")
|
||||||
export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
||||||
|
|
||||||
getChartType(): string {
|
getChartType(): string {
|
||||||
return "doughnut";
|
return "doughnut";
|
||||||
}
|
}
|
||||||
|
@ -30,12 +29,16 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
||||||
|
|
||||||
async apiRequest(): Promise<GroupMetrics> {
|
async apiRequest(): Promise<GroupMetrics> {
|
||||||
const api = new CoreApi(DEFAULT_CONFIG);
|
const api = new CoreApi(DEFAULT_CONFIG);
|
||||||
const count = (await api.coreGroupsList({
|
const count = (
|
||||||
pageSize: 1
|
await api.coreGroupsList({
|
||||||
})).pagination.count;
|
pageSize: 1,
|
||||||
const superusers = (await api.coreGroupsList({
|
})
|
||||||
isSuperuser: true
|
).pagination.count;
|
||||||
})).pagination.count;
|
const superusers = (
|
||||||
|
await api.coreGroupsList({
|
||||||
|
isSuperuser: true,
|
||||||
|
})
|
||||||
|
).pagination.count;
|
||||||
this.centerText = count.toString();
|
this.centerText = count.toString();
|
||||||
return {
|
return {
|
||||||
count: count - superusers,
|
count: count - superusers,
|
||||||
|
@ -45,24 +48,14 @@ export class GroupCountStatusChart extends AKChart<GroupMetrics> {
|
||||||
|
|
||||||
getChartData(data: GroupMetrics): ChartData {
|
getChartData(data: GroupMetrics): ChartData {
|
||||||
return {
|
return {
|
||||||
labels: [
|
labels: [t`Total groups`, t`Superuser-groups`],
|
||||||
t`Total groups`,
|
|
||||||
t`Superuser-groups`,
|
|
||||||
],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: [
|
backgroundColor: ["#2b9af3", "#3e8635"],
|
||||||
"#2b9af3",
|
|
||||||
"#3e8635",
|
|
||||||
],
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: [
|
data: [data.count, data.superusers],
|
||||||
data.count,
|
|
||||||
data.superusers,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ interface LDAPSyncStats {
|
||||||
|
|
||||||
@customElement("ak-admin-status-chart-ldap-sync")
|
@customElement("ak-admin-status-chart-ldap-sync")
|
||||||
export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
||||||
|
|
||||||
getChartType(): string {
|
getChartType(): string {
|
||||||
return "doughnut";
|
return "doughnut";
|
||||||
}
|
}
|
||||||
|
@ -36,7 +35,8 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
||||||
let healthy = 0;
|
let healthy = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
let unsynced = 0;
|
let unsynced = 0;
|
||||||
await Promise.all(sources.results.map(async (element) => {
|
await Promise.all(
|
||||||
|
sources.results.map(async (element) => {
|
||||||
try {
|
try {
|
||||||
const health = await api.sourcesLdapSyncStatusRetrieve({
|
const health = await api.sourcesLdapSyncStatusRetrieve({
|
||||||
slug: element.slug,
|
slug: element.slug,
|
||||||
|
@ -46,7 +46,7 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
||||||
}
|
}
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const maxDelta = 3600000; // 1 hour
|
const maxDelta = 3600000; // 1 hour
|
||||||
if (!health || (now - health.taskFinishTimestamp.getTime()) > maxDelta) {
|
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
|
||||||
unsynced += 1;
|
unsynced += 1;
|
||||||
} else {
|
} else {
|
||||||
healthy += 1;
|
healthy += 1;
|
||||||
|
@ -54,38 +54,26 @@ export class LDAPSyncStatusChart extends AKChart<LDAPSyncStats> {
|
||||||
} catch {
|
} catch {
|
||||||
unsynced += 1;
|
unsynced += 1;
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
this.centerText = sources.pagination.count.toString();
|
this.centerText = sources.pagination.count.toString();
|
||||||
return {
|
return {
|
||||||
healthy: sources.pagination.count === 0 ? -1 : healthy,
|
healthy: sources.pagination.count === 0 ? -1 : healthy,
|
||||||
failed,
|
failed,
|
||||||
unsynced
|
unsynced,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getChartData(data: LDAPSyncStats): ChartData {
|
getChartData(data: LDAPSyncStats): ChartData {
|
||||||
return {
|
return {
|
||||||
labels: [
|
labels: [t`Healthy sources`, t`Failed sources`, t`Unsynced sources`],
|
||||||
t`Healthy sources`,
|
|
||||||
t`Failed sources`,
|
|
||||||
t`Unsynced sources`,
|
|
||||||
],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: [
|
backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"],
|
||||||
"#3e8635",
|
|
||||||
"#C9190B",
|
|
||||||
"#2b9af3",
|
|
||||||
],
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: [
|
data: [data.healthy, data.failed, data.unsynced],
|
||||||
data.healthy,
|
|
||||||
data.failed,
|
|
||||||
data.unsynced
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ interface OutpostStats {
|
||||||
|
|
||||||
@customElement("ak-admin-status-chart-outpost")
|
@customElement("ak-admin-status-chart-outpost")
|
||||||
export class OutpostStatusChart extends AKChart<OutpostStats> {
|
export class OutpostStatusChart extends AKChart<OutpostStats> {
|
||||||
|
|
||||||
getChartType(): string {
|
getChartType(): string {
|
||||||
return "doughnut";
|
return "doughnut";
|
||||||
}
|
}
|
||||||
|
@ -36,52 +35,41 @@ export class OutpostStatusChart extends AKChart<OutpostStats> {
|
||||||
let healthy = 0;
|
let healthy = 0;
|
||||||
let outdated = 0;
|
let outdated = 0;
|
||||||
let unhealthy = 0;
|
let unhealthy = 0;
|
||||||
await Promise.all(outposts.results.map(async (element) => {
|
await Promise.all(
|
||||||
|
outposts.results.map(async (element) => {
|
||||||
const health = await api.outpostsInstancesHealthList({
|
const health = await api.outpostsInstancesHealthList({
|
||||||
uuid: element.pk || "",
|
uuid: element.pk || "",
|
||||||
});
|
});
|
||||||
if (health.length === 0) {
|
if (health.length === 0) {
|
||||||
unhealthy += 1;
|
unhealthy += 1;
|
||||||
}
|
}
|
||||||
health.forEach(h => {
|
health.forEach((h) => {
|
||||||
if (h.versionOutdated) {
|
if (h.versionOutdated) {
|
||||||
outdated += 1;
|
outdated += 1;
|
||||||
} else {
|
} else {
|
||||||
healthy += 1;
|
healthy += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
this.centerText = outposts.pagination.count.toString();
|
this.centerText = outposts.pagination.count.toString();
|
||||||
return {
|
return {
|
||||||
healthy: outposts.pagination.count === 0 ? -1 : healthy,
|
healthy: outposts.pagination.count === 0 ? -1 : healthy,
|
||||||
outdated,
|
outdated,
|
||||||
unhealthy
|
unhealthy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getChartData(data: OutpostStats): ChartData {
|
getChartData(data: OutpostStats): ChartData {
|
||||||
return {
|
return {
|
||||||
labels: [
|
labels: [t`Healthy outposts`, t`Outdated outposts`, t`Unhealthy outposts`],
|
||||||
t`Healthy outposts`,
|
|
||||||
t`Outdated outposts`,
|
|
||||||
t`Unhealthy outposts`,
|
|
||||||
],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: [
|
backgroundColor: ["#3e8635", "#f0ab00", "#C9190B"],
|
||||||
"#3e8635",
|
|
||||||
"#f0ab00",
|
|
||||||
"#C9190B",
|
|
||||||
],
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data: [
|
data: [data.healthy, data.outdated, data.unhealthy],
|
||||||
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