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-hook-form": "^7.22.0",
|
||||
"react-hot-toast": "^2.0.0",
|
||||
"react-iframe": "^1.8.0",
|
||||
"react-markdown": "^7.0.1",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-router": "6.2.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-simple-code-editor": "^0.11.0",
|
||||
"react-table": "^7.7.0",
|
||||
|
|
33
src/App.tsx
33
src/App.tsx
|
@ -1,27 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
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 { 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
|
||||
function App() {
|
||||
const { authToken, currentUser, isAdmin } = useAuth();
|
||||
|
||||
const redirectToLogin = !authToken || !currentUser?.app_roles;
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Stackspin</title>
|
||||
<title>Colli</title>
|
||||
<meta name="description" content="Stackspin" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
|
@ -32,23 +25,15 @@ function App() {
|
|||
</Helmet>
|
||||
|
||||
<div className="app bg-gray-50 min-h-screen flex flex-col">
|
||||
{redirectToLogin ? (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/login-callback" element={<LoginCallback />} />
|
||||
<Route path="*" element={<Navigate to="/login" />} />
|
||||
</Routes>
|
||||
) : (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/users" element={<ProtectedRoute />}>
|
||||
<Route path="/users" element={<Users />} />
|
||||
</Route>
|
||||
{DASHBOARD_APPS('').map((app) => (
|
||||
<Route key={app.name} path={app.internalUrl} element={<AppIframe app={app} />} />
|
||||
))}
|
||||
<Route path="*" element={<Navigate to="/dashboard" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
)}
|
||||
|
||||
{/* Place to load notifications */}
|
||||
<div
|
||||
|
|
|
@ -1,64 +1,21 @@
|
|||
import React, { Fragment, useMemo, useState } from 'react';
|
||||
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
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 _ from 'lodash';
|
||||
|
||||
import { UserModal } from '../UserModal';
|
||||
|
||||
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
const navigation = [
|
||||
{ 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
|
||||
interface 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 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 (
|
||||
<>
|
||||
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||
|
@ -84,7 +41,7 @@ const Header: React.FC<HeaderProps> = () => {
|
|||
</Link>
|
||||
<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" */}
|
||||
{navigationItems.map((item) => (
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
|
@ -101,58 +58,6 @@ const Header: React.FC<HeaderProps> = () => {
|
|||
))}
|
||||
</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>
|
||||
|
||||
|
@ -179,10 +84,6 @@ const Header: React.FC<HeaderProps> = () => {
|
|||
</div>
|
||||
)}
|
||||
</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="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>
|
||||
|
||||
<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
|
||||
href={item.url}
|
||||
key={item.name}
|
||||
|
|
|
@ -35,12 +35,19 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
|
|||
</div>
|
||||
<div className="px-2.5 py-2.5 sm:px-4 flex justify-end">
|
||||
<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"
|
||||
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>
|
||||
</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) => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Nextcloud',
|
||||
name: 'Dateiablage',
|
||||
assetSrc: '/assets/nextcloud.svg',
|
||||
markdownSrc: '/markdown/nextcloud.md',
|
||||
url: `https://files.${rootDomain}`,
|
||||
internalUrl: `files`,
|
||||
externalUrl: `https://cloud.${rootDomain}`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Wekan',
|
||||
name: 'Projektboard',
|
||||
assetSrc: '/assets/wekan.svg',
|
||||
markdownSrc: '/markdown/wekan.md',
|
||||
url: `https://wekan.${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}`,
|
||||
internalUrl: `projects`,
|
||||
externalUrl: `https://board.${rootDomain}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const DASHBOARD_QUICK_ACCESS = (rootDomain: string) => [
|
||||
export const DASHBOARD_QUICK_ACCESS = () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Monitoring →',
|
||||
url: `https://grafana.${rootDomain}`,
|
||||
description: 'Monitor your system with Grafana',
|
||||
icon: ChartBarIcon,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Support →',
|
||||
url: 'https://docs.stackspin.net',
|
||||
description: 'Access documentation and forum',
|
||||
url: 'https://support.local-it.org',
|
||||
description: 'Support',
|
||||
icon: InformationCircleIcon,
|
||||
active: true,
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue