wip: make dashboard a usable iframe test thing

This commit is contained in:
Philipp Rothmann 2022-09-12 14:18:20 +02:00
parent edb5b02608
commit 24724c1f90
10 changed files with 42211 additions and 743 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
export NODE_OPTIONS=--openssl-legacy-provider

41519
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -26,10 +26,11 @@
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-hook-form": "^7.22.0", "react-hook-form": "^7.22.0",
"react-hot-toast": "^2.0.0", "react-hot-toast": "^2.0.0",
"react-iframe": "^1.8.0",
"react-markdown": "^7.0.1", "react-markdown": "^7.0.1",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-router-dom": "6.2.1",
"react-router": "6.2.1", "react-router": "6.2.1",
"react-router-dom": "6.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-simple-code-editor": "^0.11.0", "react-simple-code-editor": "^0.11.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",

View file

@ -1,27 +1,20 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { Navigate, Route, Routes } from 'react-router-dom';
import { useAuth } from 'src/services/auth';
import { Dashboard, Users, Login } from './modules';
import { Layout } from './components'; import { Layout } from './components';
import { LoginCallback } from './modules/login/LoginCallback'; import { Dashboard } from './modules';
import { AppIframe } from './modules/dashboard/AppIframe';
import { DASHBOARD_APPS } from './modules/dashboard/consts';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
function App() { function App() {
const { authToken, currentUser, isAdmin } = useAuth();
const redirectToLogin = !authToken || !currentUser?.app_roles;
const ProtectedRoute = () => {
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
};
return ( return (
<> <>
<Helmet> <Helmet>
<title>Stackspin</title> <title>Colli</title>
<meta name="description" content="Stackspin" /> <meta name="description" content="Stackspin" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
@ -32,23 +25,15 @@ function App() {
</Helmet> </Helmet>
<div className="app bg-gray-50 min-h-screen flex flex-col"> <div className="app bg-gray-50 min-h-screen flex flex-col">
{redirectToLogin ? ( <Layout>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/dashboard" element={<Dashboard />} />
<Route path="/login-callback" element={<LoginCallback />} /> {DASHBOARD_APPS('').map((app) => (
<Route path="*" element={<Navigate to="/login" />} /> <Route key={app.name} path={app.internalUrl} element={<AppIframe app={app} />} />
))}
<Route path="*" element={<Navigate to="/dashboard" />} />
</Routes> </Routes>
) : ( </Layout>
<Layout>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/users" element={<ProtectedRoute />}>
<Route path="/users" element={<Users />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" />} />
</Routes>
</Layout>
)}
{/* Place to load notifications */} {/* Place to load notifications */}
<div <div

View file

@ -1,64 +1,21 @@
import React, { Fragment, useMemo, useState } from 'react'; import React from 'react';
import { Disclosure, Menu, Transition } from '@headlessui/react'; import { Disclosure } from '@headlessui/react';
import { MenuIcon, XIcon } from '@heroicons/react/outline'; import { MenuIcon, XIcon } from '@heroicons/react/outline';
import { useAuth } from 'src/services/auth';
import Gravatar from 'react-gravatar';
import { Link, useLocation } from 'react-router-dom';
import clsx from 'clsx'; import clsx from 'clsx';
import _ from 'lodash'; import { Link, useLocation } from 'react-router-dom';
import { UserModal } from '../UserModal';
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
const navigation = [ const navigation = [
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false }, { name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
{ name: 'Users', to: '/users', requiresAdmin: true }, { name: 'Dateiablage', to: '/files', requiresAdmin: false },
{ name: 'Projekte', to: '/projects', requiresAdmin: false },
]; ];
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ');
}
function filterNavigationByDashboardRole(isAdmin: boolean) {
if (isAdmin) {
return navigation;
}
return navigation.filter((item) => !item.requiresAdmin);
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
interface HeaderProps {} interface HeaderProps {}
const Header: React.FC<HeaderProps> = () => { const Header: React.FC<HeaderProps> = () => {
const [currentUserModal, setCurrentUserModal] = useState(false);
const [currentUserId, setCurrentUserId] = useState(null);
const { logOut, currentUser, isAdmin } = useAuth();
const { pathname } = useLocation(); const { pathname } = useLocation();
const currentUserModalOpen = (id: any) => {
setCurrentUserId(id);
setCurrentUserModal(true);
};
const currentUserModalClose = () => {
setCurrentUserModal(false);
setCurrentUserId(null);
};
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`;
}, []);
return ( return (
<> <>
<Disclosure as="nav" className="bg-white shadow relative z-10"> <Disclosure as="nav" className="bg-white shadow relative z-10">
@ -84,7 +41,7 @@ const Header: React.FC<HeaderProps> = () => {
</Link> </Link>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8"> <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */} {/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
{navigationItems.map((item) => ( {navigation.map((item) => (
<Link <Link
key={item.name} key={item.name}
to={item.to} to={item.to}
@ -101,58 +58,6 @@ const Header: React.FC<HeaderProps> = () => {
))} ))}
</div> </div>
</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">
{/* Profile dropdown */}
<Menu as="div" className="ml-3 relative">
<div>
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
<span className="sr-only">Open user menu</span>
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
<Gravatar email={currentUser?.email || undefined} size={32} rating="pg" protocol="https://" />
</span>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<a
onClick={() => currentUserModalOpen(currentUser?.id)}
className={classNames(
active ? 'bg-gray-100 cursor-pointer' : '',
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
)}
>
Configure profile
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
onClick={() => logOut()}
href={signOutUrl}
className={classNames(
active ? 'bg-gray-100 cursor-pointer' : '',
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
)}
>
Sign out
</a>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
</div> </div>
</div> </div>
@ -179,10 +84,6 @@ const Header: React.FC<HeaderProps> = () => {
</div> </div>
)} )}
</Disclosure> </Disclosure>
{currentUserModal && (
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
)}
</> </>
); );
}; };

View file

@ -0,0 +1,21 @@
import React from 'react';
import Iframe from 'react-iframe';
export const AppIframe: React.FC<any> = ({ app }: { app: any }) => {
return (
<div className="relative">
<div className="min-h-screen">
<Iframe
height="100%"
width="100%"
position="absolute"
frameBorder={0}
overflow="hidden"
scrolling="no"
title={app.name}
url={app.url}
/>
</div>
</div>
);
};

View file

@ -26,11 +26,11 @@ export const Dashboard: React.FC = () => {
<div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow"> <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"> <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">Utilities</h3> <h3 className="text-lg leading-6 font-medium text-gray-900">Weiteres</h3>
</div> </div>
<dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2"> <dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2">
{DASHBOARD_QUICK_ACCESS(rootDomain!).map((item) => ( {DASHBOARD_QUICK_ACCESS().map((item) => (
<a <a
href={item.url} href={item.url}
key={item.name} key={item.name}

View file

@ -35,12 +35,19 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
</div> </div>
<div className="px-2.5 py-2.5 sm:px-4 flex justify-end"> <div className="px-2.5 py-2.5 sm:px-4 flex justify-end">
<a <a
href={app.url} href={app.internalUrl}
rel="noreferrer"
className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Im Dashboard öffnen
</a>
<a
href={app.externalUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
> >
Launch App neues Fenster
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,50 +1,30 @@
import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/outline'; import { InformationCircleIcon } from '@heroicons/react/outline';
export const DASHBOARD_APPS = (rootDomain: string) => [ export const DASHBOARD_APPS = (rootDomain: string) => [
{ {
id: 1, id: 1,
name: 'Nextcloud', name: 'Dateiablage',
assetSrc: '/assets/nextcloud.svg', assetSrc: '/assets/nextcloud.svg',
markdownSrc: '/markdown/nextcloud.md', markdownSrc: '/markdown/nextcloud.md',
url: `https://files.${rootDomain}`, internalUrl: `files`,
externalUrl: `https://cloud.${rootDomain}`,
}, },
{ {
id: 2, id: 2,
name: 'Wekan', name: 'Projektboard',
assetSrc: '/assets/wekan.svg', assetSrc: '/assets/wekan.svg',
markdownSrc: '/markdown/wekan.md', markdownSrc: '/markdown/wekan.md',
url: `https://wekan.${rootDomain}`, internalUrl: `projects`,
}, externalUrl: `https://board.${rootDomain}`,
{
id: 3,
name: 'Zulip',
assetSrc: '/assets/zulip.svg',
markdownSrc: '/markdown/zulip.md',
url: `https://zulip.${rootDomain}`,
},
{
id: 4,
name: 'Wordpress',
assetSrc: '/assets/wordpress.svg',
markdownSrc: '/markdown/wordpress.md',
url: `https://www.${rootDomain}`,
}, },
]; ];
export const DASHBOARD_QUICK_ACCESS = (rootDomain: string) => [ export const DASHBOARD_QUICK_ACCESS = () => [
{ {
id: 1, id: 1,
name: 'Monitoring →',
url: `https://grafana.${rootDomain}`,
description: 'Monitor your system with Grafana',
icon: ChartBarIcon,
active: true,
},
{
id: 2,
name: 'Support →', name: 'Support →',
url: 'https://docs.stackspin.net', url: 'https://support.local-it.org',
description: 'Access documentation and forum', description: 'Support',
icon: InformationCircleIcon, icon: InformationCircleIcon,
active: true, active: true,
}, },

1201
yarn.lock

File diff suppressed because it is too large Load diff