introduce admin area

introduce admin area
first poc for connecting the authentik api

Co-authored-by: Philipp Rothmann <philipprothmann@posteo.de>
Reviewed-on: #2
This commit is contained in:
philipp 2022-11-08 16:36:16 +01:00
parent 8d760e588f
commit 44e4e4eb42
35 changed files with 367 additions and 133 deletions

View file

@ -1,9 +1,10 @@
tag = "$$(git describe --tags)"
build:
$(MAKE) -C backend build
docker push yksflip/dashboard-backend:latest
docker build -t dashboard .
docker tag dashboard yksflip/dashboard:latest
docker push yksflip/dashboard:latest
docker tag dashboard yksflip/dashboard:$(tag)
docker push yksflip/dashboard:$(tag)
rm:
docker stack rm ${STACK_NAME}

View file

@ -7,3 +7,4 @@ REDIRECT_URL="https://example.org/login-callback"
SECRET_KEY=
LOAD_INCLUSTER_CONFIG=false
DATABASE_URL=sqlite:///database.db
AUTHENTIK_BASEURL="https://sso.example.org/api/v3"

View file

@ -1,6 +1,9 @@
tag = "$$(git describe --tags)"
build:
docker build -t dashboard-backend .
docker tag dashboard-backend yksflip/dashboard-backend:latest
docker tag dashboard-backend yksflip/dashboard-backend:$(tag)
docker push yksflip/dashboard-backend:$(tag)
clean:
rm database.db

View file

@ -1,32 +0,0 @@
"""Everything to do with Apps"""
from database import db
from .models import App
class LITApp(App):
"""
"""
def get_url(self):
return self.url
def to_dict(self):
"""
represent this object as a dict, compatible for JSON output
"""
return {"id": self.id,
"name": self.name,
"slug": self.slug,
"external": self.external,
"status": self.get_status(),
"url": self.get_url()}
def get_status(self):
"""Returns an AppStatus object that describes the current cluster state"""
return {
"installed": "",
"ready": "",
"message": "",
}

View file

@ -5,15 +5,14 @@ from flask_cors import cross_origin
from datetime import timedelta
from areas import api_v1
from areas.apps import App, AppRole
from config import *
from helpers import HydraOauth, BadRequest
from helpers import LITOauth, BadRequest
@api_v1.route("/login", methods=["POST"])
@cross_origin()
def login():
authorization_url = HydraOauth.authorize()
authorization_url = LITOauth.authorize()
return jsonify({"authorizationUrl": authorization_url})
@ -28,26 +27,17 @@ def hydra_callback():
if code == None:
raise BadRequest("Missing code query param")
token = HydraOauth.get_token(state, code)
user_info = HydraOauth.get_user_info()
token = LITOauth.get_token(state, code)
user_info = LITOauth.get_user_info()
access_token = create_access_token(
identity=token, expires_delta=timedelta(days=365),
#additional_claims={"user_id": identity["id"]}
)
# apps = App.query.all()
# app_roles = []
# for app in apps:
# tmp_app_role = AppRole.query.filter_by(
# user_id=identity["id"], app_id=app.id
# ).first()
# app_roles.append(
# {
# "name": app.slug,
# "role_id": tmp_app_role.role_id if tmp_app_role else None,
# }
# )
identity=token, expires_delta=timedelta(days=365))
isAdmin = "admin" in user_info["groups"]
app_roles = [
{
"name": "dashboard",
"role_id": 1 if isAdmin else 2
},
]
return jsonify(
{
@ -57,7 +47,7 @@ def hydra_callback():
"email": user_info["email"],
"name": user_info["name"],
"preferredUsername": user_info["preferred_username"],
# "app_roles": app_roles,
},
"app_roles": app_roles
}
}
)

View file

@ -0,0 +1,39 @@
from flask import current_app
from helpers.authentik_api import AuthentikApi
from .user_service import UserService
from .models import User
class UserService(UserService):
@staticmethod
def get_users():
user_list = [User.from_authentik(u).to_dict() for u in AuthentikApi.get("/core/users")]
return user_list
@staticmethod
def get_user(id):
pass
@staticmethod
def post_user(data):
pass
@staticmethod
def __start_recovery_flow(email):
pass
@staticmethod
def put_user(id, user_editing_id, data):
pass
@staticmethod
def delete_user(id):
pass
@staticmethod
def post_multiple_users(data):
pass
@staticmethod
def __insertAppRoleToUser(userId, userRes):
pass

View file

@ -0,0 +1,37 @@
class User:
id = None
uuid = None
traits = None
email = None
name = None
preferredUsername = None
state = None
def __init__(self):
pass
@staticmethod
def from_authentik(authentik_user):
u = User()
u.id = authentik_user["pk"]
u.uuid = authentik_user["uid"]
u.name = authentik_user["name"]
u.email = authentik_user["email"]
u.traits = {
"name": authentik_user["name"],
"email": authentik_user["email"],
"app_roles": []
}
u.preferredUsername = authentik_user["username"]
u.state = "active" if authentik_user["is_active"] else ""
return u
def to_dict(self):
return {
"id": self.id,
"traits": self.traits,
"preferredUsername": self.preferredUsername,
"state": self.state,
}

View file

@ -8,16 +8,15 @@ from helpers import KratosApi
from helpers.auth_guard import admin_required
from .validation import schema, schema_multiple
from .user_service import UserService
from .lit_user_service import UserService
@api_v1.route("/users", methods=["GET"])
@jwt_required()
@cross_origin()
@admin_required()
# @admin_required() TODO: not needed as authentik checks permissions?
def get_users():
res = UserService.get_users()
return jsonify(res)
return jsonify(UserService.get_users())
@api_v1.route("/users/<string:id>", methods=["GET"])

View file

@ -1,11 +1,13 @@
import os
def env_file(key: str):
file_env = os.environ.get(f"{key}_FILE")
if file_env and os.path.exists(file_env):
return open(file_env).read().rstrip('\n')
return os.environ.get(key)
SECRET_KEY = env_file("SECRET_KEY")
HYDRA_CLIENT_ID = os.environ.get("HYDRA_CLIENT_ID")
@ -28,4 +30,7 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
# Set this to "true" to load the config from a Kubernetes serviceaccount
# running in a Kubernetes pod. Set it to "false" to load the config from the
# `KUBECONFIG` environment variable.
LOAD_INCLUSTER_CONFIG = os.environ.get("LOAD_INCLUSTER_CONFIG").lower() == "true"
LOAD_INCLUSTER_CONFIG = os.environ.get(
"LOAD_INCLUSTER_CONFIG").lower() == "true"
AUTHENTIK_BASEURL = os.environ.get("AUTHENTIK_BASEURL")

View file

@ -2,3 +2,4 @@ from .kratos_api import *
from .error_handler import *
from .hydra_oauth import *
from .kratos_user import *
from .lit_oauth import *

View file

@ -12,6 +12,7 @@ def admin_required():
def decorator(*args, **kwargs):
verify_jwt_in_request()
claims = get_jwt()
user_id = claims["user_id"]
is_admin = RoleService.is_user_admin(user_id)
if is_admin:

View file

@ -0,0 +1,42 @@
from typing import List
from flask_jwt_extended import get_jwt
import requests
from config import AUTHENTIK_BASEURL
from .error_handler import AuthentikError
class AuthentikApi: # TODO: check if can be replaced with apispec generated api?
@staticmethod
def __handleError(res):
if res.status_code >= 400:
message = res.json()["error"]["message"]
raise AuthentikError(message, res.status_code)
@staticmethod
def __token():
jwt = get_jwt()
return jwt["sub"]["refresh_token"]
@staticmethod
def get(url):
try:
res = requests.get(f"{AUTHENTIK_BASEURL}{url}", headers={
"Authorization": f"Bearer {AuthentikApi.__token()}"})
AuthentikApi.__handleError(res)
if ("pagination" in res.json()):
return AuthentikApi.__paginate(res)
return res.json()
except AuthentikError as err:
raise err
except:
raise AuthentikError()
@staticmethod
def __paginate(res: requests.Response): # TODO: test this
results = res.json()["results"]
for page in range(1, res.json()["pagination"]["total_pages"]):
res = requests.get(
f"{res.request.url}", headers=res.request.headers, params={'page': page})
AuthentikApi.__handleError(res)
results.append(res.json()["results"])
return results

View file

@ -5,6 +5,8 @@ from jsonschema import ValidationError
class KratosError(Exception):
pass
class AuthentikError(Exception):
pass
class HydraError(Exception):
pass

View file

@ -9,7 +9,7 @@ class HydraOauth:
@staticmethod
def authorize():
try:
hydra = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL)
hydra = OAuth2Session(HYDRA_CLIENT_ID)
authorization_url, state = hydra.authorization_url(
HYDRA_AUTHORIZATION_BASE_URL
)

View file

@ -4,7 +4,6 @@ import requests
from config import *
from .error_handler import KratosError
class KratosApi:
@staticmethod
def __handleError(res):

View file

@ -0,0 +1,51 @@
from flask import request, session
from requests_oauthlib import OAuth2Session
from config import *
from helpers import HydraError
class LITOauth:
@staticmethod
def authorize():
try:
scopes = ["openid", "email", "profile", "goauthentik.io/api"]
oauth = OAuth2Session(HYDRA_CLIENT_ID, redirect_uri=REDIRECT_URL, scope=scopes)
authorization_url, state = oauth.authorization_url(
HYDRA_AUTHORIZATION_BASE_URL
)
return authorization_url
except Exception as err:
raise HydraError(str(err), 500)
@staticmethod
def get_token(state, code):
try:
oauth = OAuth2Session(
client_id=HYDRA_CLIENT_ID,
state=state,
)
token = oauth.fetch_token(
token_url=TOKEN_URL,
code=code,
client_secret=HYDRA_CLIENT_SECRET,
include_client_id=True,
)
session["oauth_token"] = token
return token
except Exception as err:
raise HydraError(str(err), 500)
@staticmethod
def get_user_info():
try:
hydra = OAuth2Session(
client_id=HYDRA_CLIENT_ID, token=session["oauth_token"]
)
user_info = hydra.get("{}/userinfo".format(HYDRA_PUBLIC_URL))
return user_info.json()
except Exception as err:
raise HydraError(str(err), 500)

View file

@ -1,5 +1,8 @@
alembic==1.8.1
attrs==21.4.0
black==22.1.0
cachelib==0.9.0
cachetools==5.2.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
@ -9,32 +12,43 @@ Flask==2.0.3
Flask-Cors==3.0.10
flask-expects-json==1.7.0
Flask-JWT-Extended==4.3.1
Flask-Migrate==3.1.0
Flask-Session==0.4.0
Flask-SQLAlchemy==2.5.1
google-auth==2.14.0
greenlet==2.0.0.post0
gunicorn==20.1.0
hydra-client==0.4.0
idna==3.3
install==1.3.5
itsdangerous==2.1.1
jsonschema==4.4.0
Jinja2==3.0.3
jinja2-base64-filters==0.1.4
jsonschema==4.4.0
kubernetes==24.2.0
Mako==1.2.3
MarkupSafe==2.1.1
mypy-extensions==0.4.3
oauthlib==3.2.0
ory-kratos-client==0.9.0a2
pathspec==0.9.0
platformdirs==2.5.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21
PyJWT==2.3.0
PyMySQL==1.0.2
pyrsistent==0.18.1
python-dateutil==2.8.2
PyYAML==6.0
regex==2022.3.15
requests==2.27.1
requests-oauthlib==1.3.1
rsa==4.9
six==1.16.0
SQLAlchemy==1.4.42
tomli==1.2.3
typing-extensions==4.1.1
typing_extensions==4.1.1
urllib3==1.26.8
websocket-client==1.4.2
Werkzeug==2.0.3
ory-kratos-client==0.9.0a2
pymysql
Flask-SQLAlchemy
hydra-client
Flask-Migrate

View file

@ -1,6 +1,6 @@
/* base.js
This is the base JS file to render the user interfaces of kratos and provide
the end user with flows for login, recovery etc.
the end user with flows for login, recovery etc.
check_flow_*():
These functions check the status of the flow and based on the status do some
@ -261,8 +261,6 @@ function render_messages(data) {
// value: If there is already a value known, show it
// messages: error messages related to the field
function getFormElement(type, name, value, messages) {
console.log('Getting form element', type, name, value, messages);
if (value == undefined) {
value = '';
}
@ -350,7 +348,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
if (typeof help == 'undefined' || help == null) {
help = '';
}
console.log('Messages: ', messages);
// Id field for help element
var nameHelp = name + 'Help';
@ -362,7 +359,6 @@ function getFormInput(type, name, value, label, placeHolder, help, messages) {
// messages get appended to help info
if (messages.length) {
for (message in messages) {
console.log('adding message', messages[message]);
help += messages[message]['text'];
}
}

30
public/assets/zammad.svg Normal file
View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="126px" height="108px" viewBox="0 0 42 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>logo</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="logo" sketch:type="MSArtboardGroup">
<g sketch:type="MSLayerGroup" transform="translate(1.000000, 0.000000)" id="Shape">
<path d="M27.3375,12.6 L36.72,9.72 L31.1625,13.2525 L27.3375,12.6 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M33.0525,19.62 L31.1625,13.2525 L36.72,9.72 L35.055,15.435 L33.0525,19.62 Z" fill="#E84F83" sketch:type="MSShapeGroup"></path>
<path d="M39.465,7.9875 L38.43,9.72 L35.055,15.435 L36.72,9.72 L39.465,7.9875 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M39.8025,9.1125 L37.1925,11.79 L38.43,9.72 L39.8025,9.1125 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M27.9,10.8225 L35.5725,10.0575 L30.24,11.7 L27.9,10.8225 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M28.1925,15.165 L31.1625,13.2525 L33.0525,19.62 L32.0625,21.645 L28.1925,15.165 Z" fill="#CA2317" sketch:type="MSShapeGroup"></path>
<path d="M23.76,22.725 L22.3425,5.4 L32.0625,21.645 L23.76,22.725 Z" fill="#B7DFF2" sketch:type="MSShapeGroup"></path>
<path d="M19.7325,27.1575 L23.76,22.725 L32.0625,21.645 L19.7325,27.1575 Z" fill="#E54011" sketch:type="MSShapeGroup"></path>
<path d="M0.1575,35.865 L19.7325,27.1575 L23.76,22.725 L17.37,22.0725 L0.1575,35.865 Z" fill="#FFCE33" sketch:type="MSShapeGroup"></path>
<path d="M0.9,28.755 L10.9575,27.225 L14.085,24.705 L12.555,24.03 L0.9,28.755 Z" fill="#D6B12D" sketch:type="MSShapeGroup"></path>
<path d="M4.5225,20.5425 L14.085,24.705 L17.37,22.0725 L4.5225,20.5425 Z" fill="#FFDE85" sketch:type="MSShapeGroup"></path>
<path d="M21.6225,11.6775 L20.4075,11.88 L17.37,22.0725 L20.655,20.0025 L21.6225,11.6775 Z" fill="#009EC6" sketch:type="MSShapeGroup"></path>
<path d="M23.4,18.2475 L20.655,20.0025 L22.3425,5.4 L23.4,18.2475 Z" fill="#5EAFCE" sketch:type="MSShapeGroup"></path>
<path d="M13.0275,13.05 L21.6225,11.6775 L22.005,8.28 L13.0275,13.05 Z" fill="#045972" sketch:type="MSShapeGroup"></path>
<path d="M12.105,5.085 L19.575,9.585 L22.005,8.28 L22.0725,7.8075 L12.105,5.085 Z" fill="#5A8591" sketch:type="MSShapeGroup"></path>
<path d="M13.5675,0.18 L20.3625,7.335 L22.0725,7.8075 L22.3425,5.4 L13.5675,0.18 Z" fill="#009EC6" sketch:type="MSShapeGroup"></path>
<path d="M17.37,22.0725 L23.4,18.2475 L23.76,22.725 L17.37,22.0725 Z" fill="#F39804" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
public/env.js vendored
View file

@ -1,3 +1,4 @@
window.env = {
REACT_APP_API_URL: 'http://localhost:5000/api/v1',
REACT_APP_SSO_LOGOUT_URL: 'https://login.example.org/if/flow/default-invalidation-flow/'
};

View file

@ -0,0 +1 @@
Verwalte User und Gruppen

View file

@ -0,0 +1 @@
Ticketsystem

View file

@ -13,7 +13,6 @@ import { Login } from './modules/login';
import { Users } from './modules/users/Users';
import { AppSingle } from './modules/apps/AppSingle';
import { Apps } from './modules/apps/Apps';
import { DashboardLIT } from './modules/dashboard/DashboardLIT';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function App() {
@ -53,7 +52,7 @@ function App() {
) : (
<Layout>
<Routes>
<Route path="/dashboard" element={<DashboardLIT />} />
<Route path="/dashboard" element={<Dashboard />} />
{apps.map((app) => (
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
))}

View file

@ -1,4 +1,4 @@
import React, { Fragment, useMemo, useState } from 'react';
import React, { Fragment, useMemo } from 'react';
import { Disclosure, Menu, Transition } from '@headlessui/react';
import { MenuIcon, XIcon } from '@heroicons/react/outline';
import { useAuth } from 'src/services/auth';
@ -6,16 +6,10 @@ import Gravatar from 'react-gravatar';
import { Link, useLocation } from 'react-router-dom';
import clsx from 'clsx';
import { useApps } from 'src/services/apps';
import _ from 'lodash';
import { UserModal } from '../UserModal';
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
const navigation = [
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
{ name: 'Users', to: '/users', requiresAdmin: true },
{ name: 'Apps', to: '/apps', requiresAdmin: true },
{ name: '', to: '/users', requiresAdmin: true },
// { name: 'Apps', to: '/apps', requiresAdmin: true },
];
function classNames(...classes: any[]) {
@ -34,32 +28,15 @@ function filterNavigationByDashboardRole(isAdmin: boolean) {
interface HeaderProps {}
const HeaderLIT: React.FC<HeaderProps> = () => {
const [currentUserModal, setCurrentUserModal] = useState(false);
const [currentUserId, setCurrentUserId] = useState(null);
const { logOut, currentUser, isAdmin } = useAuth();
const { pathname } = useLocation();
const { apps, loadApps, appTableLoading } = useApps();
const currentUserModalOpen = (id: any) => {
setCurrentUserId(id);
setCurrentUserModal(true);
};
const currentUserModalClose = () => {
setCurrentUserModal(false);
setCurrentUserId(null);
};
const { apps } = useApps();
const navigationItems = filterNavigationByDashboardRole(isAdmin);
const signOutUrl = useMemo(() => {
const { hostname } = window.location;
// If we are developing locally, we need to use the init cluster's public URL
if (hostname === 'localhost') {
return HYDRA_LOGOUT_URL;
}
return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`;
// @ts-ignore
return window.env.REACT_APP_SSO_LOGOUT_URL;
}, []);
return (
@ -102,6 +79,21 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
{app.name}
</Link>
))}
{/* {navigationItems.map((item) => (
<Link
key={item.name}
to={item.to}
className={clsx(
'border-primary-50 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium litbutton',
{
'border-primary-500 litbutton-active hover:border-gray-300 inline-flex items-center px-1 pt-1 text-sm font-medium':
pathname.includes(item.to),
},
)}
>
{item.name}
</Link>
))} */}
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
@ -167,10 +159,6 @@ const HeaderLIT: React.FC<HeaderProps> = () => {
</div>
)}
</Disclosure>
{currentUserModal && (
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
)}
</>
);
};

View file

@ -1,2 +1 @@
export { default as Header } from './Header';
export { default as HeaderLIT } from './HeaderLIT';
export { default as Header } from './HeaderLIT';

View file

@ -1,10 +1,10 @@
import React from 'react';
import { HeaderLIT } from '../Header';
import { Header } from '../Header';
const Layout: React.FC = ({ children }) => {
return (
<>
<HeaderLIT />
<Header />
{children}
</>

View file

@ -1,5 +1,5 @@
export { Layout } from './Layout';
export { Header, HeaderLIT } from './Header';
export { Header } from './Header';
export { Table } from './Table';
export { Banner } from './Banner';
export { Tabs } from './Tabs';

View file

@ -1,9 +1,13 @@
import React, { useEffect } from 'react';
import { useApps } from 'src/services/apps';
import { DashboardCardLIT } from './components/DashboardCard/DashboardCardLIT';
import { AppStatusEnum, useApps } from 'src/services/apps';
import { useAuth } from 'src/services/auth';
import { DashboardUtility } from './components';
import { DashboardCard } from './components/DashboardCard/DashboardCardLIT';
import { UTILITY_APPS } from './consts';
export const DashboardLIT: React.FC = () => {
export const Dashboard: React.FC = () => {
const { apps, loadApps, appTableLoading } = useApps();
const { isAdmin } = useAuth();
// Tell React to load the apps
useEffect(() => {
@ -21,9 +25,27 @@ export const DashboardLIT: React.FC = () => {
</div>
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10">
{!appTableLoading && apps.map((app) => <DashboardCardLIT app={app} key={app.name} />)}
{!appTableLoading &&
apps
.filter((app) => UTILITY_APPS.indexOf(app.slug) === -1)
.map((app) => <DashboardCard app={app} key={app.name} />)}
</div>
</div>
{isAdmin && (
<div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
<div className="pb-4 border-b border-gray-200 sm:flex sm:items-center">
<h3 className="text-lg leading-6 font-medium text-gray-900">Administration</h3>
</div>
<dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2">
{apps
.filter((app) => UTILITY_APPS.indexOf(app.slug) !== -1 && app.url !== null)
.map((app) => (
<DashboardUtility item={app} key={app.name} />
))}
</dl>
</div>
)}
</div>
);
};

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
export const DashboardCardLIT: React.FC<any> = ({ app }: { app: any }) => {
export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
return (
<>
<Link

View file

@ -1 +1 @@
export { DashboardCard } from './DashboardCard';
export { DashboardCard } from './DashboardCardLIT';

View file

@ -0,0 +1,36 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
export const DashboardUtility: React.FC<any> = ({ item }: { item: any }) => {
const [content, setContent] = useState('');
useEffect(() => {
fetch(item.markdownSrc)
.then((res) => res.text())
.then((md) => {
return setContent(md);
})
.catch(() => {});
}, [item.markdownSrc]);
return (
<Link
to={`/${item.slug}`}
key={item.name}
rel="noreferrer"
className="bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group"
>
<div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
{item.icon && <item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />}
{item.assetSrc && <img className="h-6 w-6" src={item.assetSrc} alt={item.name} />}
</div>
<div>
<dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
<dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">
<ReactMarkdown>{content}</ReactMarkdown>
</dd>
</div>
</Link>
);
};

View file

@ -1 +1 @@
export { DashboardUtility } from './DashboardUtility';
export { DashboardUtility } from './DashboardUtilityLIT';

View file

@ -10,7 +10,7 @@ export const DASHBOARD_QUICK_ACCESS = [
];
/** Apps that should not be shown on the dashboard */
export const HIDDEN_APPS = ['dashboard', 'velero'];
export const HIDDEN_APPS = ['dashboard'];
/** Apps that should be shown under "Utilities" */
export const UTILITY_APPS = ['monitoring'];
export const UTILITY_APPS = ['authentik', 'zammad'];

View file

@ -1 +1 @@
export { Dashboard } from './Dashboard';
export { Dashboard } from './DashboardLIT';

View file

@ -53,6 +53,11 @@ export const Users: React.FC = () => {
const columns: any = React.useMemo(
() => [
{
Header: 'Username',
accessor: 'preferredUsername',
width: 'auto',
},
{
Header: 'Name',
accessor: 'name',
@ -75,11 +80,12 @@ export const Users: React.FC = () => {
if (isAdmin) {
return (
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
<div className="text-right lg:opacity-0 transition-opacity">
<button
disabled
onClick={() => configureModalOpen(row.original.id)}
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray bg-gray hover:bg-gray focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Configure
@ -109,17 +115,19 @@ export const Users: React.FC = () => {
{isAdmin && (
<div className="mt-3 sm:mt-0 sm:ml-4">
<button
disabled
onClick={() => configureModalOpen(null)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
>
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new user
</button>
<button
disabled
onClick={() => setMultipleUsersModal(true)}
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
>
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
Add new users