add-frames (#1)
Adds a iframe view for apps in the dashboard. Makes it usable for our setup. Co-authored-by: Philipp Rothmann <philipprothmann@posteo.de> Co-authored-by: viehlieb <pf@pragma-shift.net> Reviewed-on: #1
This commit is contained in:
parent
696ffba9fe
commit
dea8773ff6
63 changed files with 1408 additions and 896 deletions
33
src/App.tsx
33
src/App.tsx
|
|
@ -1,31 +1,43 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import { Dashboard, Users, Login, Apps, AppSingle } from './modules';
|
||||
import { Dashboard } from './modules';
|
||||
import { Layout } from './components';
|
||||
import { AppIframe } from './modules/dashboard/AppIframe';
|
||||
import { LoginCallback } from './modules/login/LoginCallback';
|
||||
import { useApps } from './services/apps';
|
||||
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() {
|
||||
const { authToken, currentUser, isAdmin } = useAuth();
|
||||
|
||||
const redirectToLogin = !authToken || !currentUser?.app_roles;
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
|
||||
};
|
||||
|
||||
const { apps, loadApps } = useApps();
|
||||
|
||||
useEffect(() => {
|
||||
loadApps();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Stackspin</title>
|
||||
<title>Dashboard</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" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon_lit_transp.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/lit_logos/lit_transp_32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/lit_logos/lit_transp_16x16" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
|
@ -41,7 +53,10 @@ function App() {
|
|||
) : (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/dashboard" element={<DashboardLIT />} />
|
||||
{apps.map((app) => (
|
||||
<Route key={app.name} path={app.slug} element={<AppIframe app={app} />} />
|
||||
))}
|
||||
<Route path="/users" element={<ProtectedRoute />}>
|
||||
<Route index element={<Users />} />
|
||||
</Route>
|
||||
|
|
|
|||
178
src/components/Header/HeaderLIT.tsx
Normal file
178
src/components/Header/HeaderLIT.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import React, { Fragment, useMemo, useState } from 'react';
|
||||
import { Disclosure, Menu, Transition } 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 { 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 },
|
||||
];
|
||||
|
||||
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 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 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">
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex justify-between h-10">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||
<img className="block lg:hidden" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||
<img className="hidden lg:block" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||
</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" */}
|
||||
{apps.map((app) => (
|
||||
<Link
|
||||
key={app.name}
|
||||
to={app.slug}
|
||||
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(app.slug),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{app.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">
|
||||
{/* 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={() => logOut()}
|
||||
href={signOutUrl}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-2 pb-4 space-y-1">
|
||||
{apps.map((app) => (
|
||||
<Link
|
||||
key={app.name}
|
||||
to={app.slug}
|
||||
className={clsx(
|
||||
'border-transparent litbutton block pl-3 pr-4 py-2 border-l-4 litbutton text-base font-medium',
|
||||
{
|
||||
'litbutton-active border-primary-400 block pl-3 pr-4 py-2': pathname.includes(app.slug),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{app.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
{currentUserModal && (
|
||||
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderLIT;
|
||||
|
|
@ -1 +1,2 @@
|
|||
export { default as Header } from './Header';
|
||||
export { default as HeaderLIT } from './HeaderLIT';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Header } from '../Header';
|
||||
import { HeaderLIT } from '../Header';
|
||||
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<HeaderLIT />
|
||||
|
||||
{children}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
export { Layout } from './Layout';
|
||||
export { Header } from './Header';
|
||||
export { Header, HeaderLIT } from './Header';
|
||||
export { Table } from './Table';
|
||||
export { Banner } from './Banner';
|
||||
export { Tabs } from './Tabs';
|
||||
export { Modal, ConfirmationModal, StepsModal } from './Modal';
|
||||
export { UserModal } from './UserModal';
|
||||
export { ProgressSteps } from './ProgressSteps';
|
||||
|
|
|
|||
1
src/index.css
vendored
1
src/index.css
vendored
|
|
@ -1,6 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./lit_navigation_style.css";
|
||||
|
||||
div[tabindex] {
|
||||
flex: 1;
|
||||
|
|
|
|||
57
src/lit_navigation_style.css
vendored
Normal file
57
src/lit_navigation_style.css
vendored
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
.litbutton{
|
||||
color: #755d86;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.litbutton-card{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.litbutton-card:before,
|
||||
.litbutton:before {
|
||||
content: "[";
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
-webkit-transform: translateX(20px);
|
||||
-moz-transform: translateX(20px);
|
||||
transform: translateX(20px);
|
||||
-webkit-transition: -webkit-transform 0.3s, opacity 0.2s;
|
||||
-moz-transition: -moz-transform 0.3s, opacity 0.2s;
|
||||
transition: transform 0.3s, opacity 0.2s;
|
||||
}
|
||||
.litbutton:before{
|
||||
margin-right: 10px;
|
||||
}
|
||||
.litbutton-card:after,
|
||||
.litbutton:after {
|
||||
content: "]";
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s, opacity 0.2s;
|
||||
-moz-transition: -moz-transform 0.3s, opacity 0.2s;
|
||||
transition: transform 0.3s, opacity 0.2s;
|
||||
-webkit-transform: translateX(-20px);
|
||||
-moz-transform: translateX(-20px);
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
.litbutton:after{
|
||||
margin-left: 10px;
|
||||
|
||||
}
|
||||
.litbutton-card:hover,
|
||||
.litbutton:hover{
|
||||
color: #3a97a3;
|
||||
}
|
||||
.litbutton-card:active,
|
||||
.litbutton-active{
|
||||
color: #3a97a3;
|
||||
}
|
||||
.litbutton-card:hover:before,
|
||||
.litbutton-card:hover:after,
|
||||
.litbutton:hover:before,
|
||||
.litbutton:hover:after {
|
||||
color: #3a97a3;
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(0px);
|
||||
-moz-transform: translateX(0px);
|
||||
transform: translateX(0px);
|
||||
}
|
||||
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 style={{ minHeight: '95vh' }}>
|
||||
<Iframe
|
||||
height="100%"
|
||||
width="100%"
|
||||
position="absolute"
|
||||
frameBorder={0}
|
||||
overflow="hidden"
|
||||
scrolling="no"
|
||||
title={app.name}
|
||||
url={app.url}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
29
src/modules/dashboard/DashboardLIT.tsx
Normal file
29
src/modules/dashboard/DashboardLIT.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useApps } from 'src/services/apps';
|
||||
import { DashboardCardLIT } from './components/DashboardCard/DashboardCardLIT';
|
||||
|
||||
export const DashboardLIT: React.FC = () => {
|
||||
const { apps, loadApps, appTableLoading } = useApps();
|
||||
|
||||
// Tell React to load the apps
|
||||
useEffect(() => {
|
||||
loadApps();
|
||||
|
||||
return () => {};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8">
|
||||
<div className="mt-6 pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Dashboard</h1>
|
||||
</div>
|
||||
</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} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const DashboardCardLIT: React.FC<any> = ({ app }: { app: any }) => {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to={`/${app.slug}`}
|
||||
// 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"
|
||||
>
|
||||
<div
|
||||
className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0"
|
||||
key={app.name}
|
||||
>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
|
||||
src={app.assetSrc}
|
||||
alt={app.name}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl litbutton-card leading-8 font-bold">{app.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -29,8 +29,8 @@ export function Login() {
|
|||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
|
||||
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
|
||||
<img className="lg:block" src="assets/lit_logos/lit_transp_title_96.png" alt="Local-IT" />
|
||||
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Einloggen</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
|
|
@ -42,7 +42,7 @@ export function Login() {
|
|||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
|
||||
</span>
|
||||
Sign in
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function LoginCallback() {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
|
||||
<p className="text-lg text-primary-600 mt-2">Du wirst jetzt eingeloggt.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export const api = {
|
||||
hostname: process.env.REACT_APP_API_URL,
|
||||
// @ts-ignore
|
||||
hostname: window.env.REACT_APP_API_URL,
|
||||
};
|
||||
|
|
|
|||
11
src/services/apps/api.ts
Normal file
11
src/services/apps/api.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { performApiCall } from '../api';
|
||||
import { transformApp } from './transformations';
|
||||
import { App } from './types';
|
||||
|
||||
export const fetchApps = async (): Promise<App> => {
|
||||
// @ts-ignore
|
||||
const apiUrl = window.env.REACT_APP_API_URL;
|
||||
|
||||
const res = await performApiCall({ hostname: apiUrl, path: '/apps', method: 'GET' });
|
||||
return transformApp(res);
|
||||
};
|
||||
Reference in a new issue