wip: make dashboard a usable iframe test thing
This commit is contained in:
parent
edb5b02608
commit
24724c1f90
10 changed files with 42211 additions and 743 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export NODE_OPTIONS=--openssl-legacy-provider
|
41519
package-lock.json
generated
Normal file
41519
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
41
src/App.tsx
41
src/App.tsx
|
@ -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
|
||||||
|
|
|
@ -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} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
21
src/modules/dashboard/AppIframe.tsx
Normal file
21
src/modules/dashboard/AppIframe.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue