mirror of
https://github.com/pomerium/pomerium.git
synced 2025-07-19 01:28:51 +02:00
proxy: add routes HTML page (#5443)
* proxy: add route portal json * fix 405 issue * proxy: add routes HTML page
This commit is contained in:
parent
e9786f9719
commit
97ba21b95a
11 changed files with 269 additions and 44 deletions
|
@ -241,6 +241,7 @@ var internalPathsNeedingLogin = set.From([]string{
|
||||||
"/.pomerium/jwt",
|
"/.pomerium/jwt",
|
||||||
"/.pomerium/user",
|
"/.pomerium/user",
|
||||||
"/.pomerium/webauthn",
|
"/.pomerium/webauthn",
|
||||||
|
"/.pomerium/routes",
|
||||||
"/.pomerium/api/v1/routes",
|
"/.pomerium/api/v1/routes",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ func (p *Proxy) registerDashboardHandlers(r *mux.Router, opts *config.Options) *
|
||||||
if opts.IsRuntimeFlagSet(config.RuntimeFlagPomeriumJWTEndpoint) {
|
if opts.IsRuntimeFlagSet(config.RuntimeFlagPomeriumJWTEndpoint) {
|
||||||
h.Path("/jwt").Handler(httputil.HandlerFunc(p.jwtAssertion)).Methods(http.MethodGet)
|
h.Path("/jwt").Handler(httputil.HandlerFunc(p.jwtAssertion)).Methods(http.MethodGet)
|
||||||
}
|
}
|
||||||
|
h.Path("/routes").Handler(httputil.HandlerFunc(p.routesPortalHTML)).Methods(http.MethodGet)
|
||||||
h.Path("/sign_out").Handler(httputil.HandlerFunc(p.SignOut)).Methods(http.MethodGet, http.MethodPost)
|
h.Path("/sign_out").Handler(httputil.HandlerFunc(p.SignOut)).Methods(http.MethodGet, http.MethodPost)
|
||||||
h.Path("/user").Handler(httputil.HandlerFunc(p.jsonUserInfo)).Methods(http.MethodGet)
|
h.Path("/user").Handler(httputil.HandlerFunc(p.jsonUserInfo)).Methods(http.MethodGet)
|
||||||
h.Path("/webauthn").Handler(p.webauthn)
|
h.Path("/webauthn").Handler(p.webauthn)
|
||||||
|
|
|
@ -8,8 +8,17 @@ import (
|
||||||
"github.com/pomerium/pomerium/internal/handlers"
|
"github.com/pomerium/pomerium/internal/handlers"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/proxy/portal"
|
"github.com/pomerium/pomerium/proxy/portal"
|
||||||
|
"github.com/pomerium/pomerium/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (p *Proxy) routesPortalHTML(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
u := p.getUserInfoData(r)
|
||||||
|
rs := p.getPortalRoutes(u)
|
||||||
|
m := u.ToJSON()
|
||||||
|
m["routes"] = rs
|
||||||
|
return ui.ServePage(w, r, "Routes", "Routes Portal", m)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Proxy) routesPortalJSON(w http.ResponseWriter, r *http.Request) error {
|
func (p *Proxy) routesPortalJSON(w http.ResponseWriter, r *http.Request) error {
|
||||||
u := p.getUserInfoData(r)
|
u := p.getUserInfoData(r)
|
||||||
rs := p.getPortalRoutes(u)
|
rs := p.getPortalRoutes(u)
|
||||||
|
|
|
@ -4,15 +4,16 @@ import React, { FC, useLayoutEffect } from "react";
|
||||||
import ErrorPage from "./components/ErrorPage";
|
import ErrorPage from "./components/ErrorPage";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
|
import RoutesPage from "./components/RoutesPage";
|
||||||
import SignOutConfirmPage from "./components/SignOutConfirmPage";
|
import SignOutConfirmPage from "./components/SignOutConfirmPage";
|
||||||
import SignedOutPage from "./components/SignedOutPage";
|
import SignedOutPage from "./components/SignedOutPage";
|
||||||
import { ToolbarOffset } from "./components/ToolbarOffset";
|
import { ToolbarOffset } from "./components/ToolbarOffset";
|
||||||
|
import UpstreamErrorPage from "./components/UpstreamErrorPage";
|
||||||
import UserInfoPage from "./components/UserInfoPage";
|
import UserInfoPage from "./components/UserInfoPage";
|
||||||
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
||||||
import { SubpageContextProvider } from "./context/Subpage";
|
import { SubpageContextProvider } from "./context/Subpage";
|
||||||
import { createTheme } from "./theme";
|
import { createTheme } from "./theme";
|
||||||
import { PageData } from "./types";
|
import { PageData } from "./types";
|
||||||
import UpstreamErrorPage from "./components/UpstreamErrorPage";
|
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const data = (window["POMERIUM_DATA"] || {}) as PageData;
|
const data = (window["POMERIUM_DATA"] || {}) as PageData;
|
||||||
|
@ -20,8 +21,11 @@ const App: FC = () => {
|
||||||
const secondary = data?.secondaryColor || "#49AAA1";
|
const secondary = data?.secondaryColor || "#49AAA1";
|
||||||
const theme = createTheme(primary, secondary);
|
const theme = createTheme(primary, secondary);
|
||||||
let body: React.ReactNode = <></>;
|
let body: React.ReactNode = <></>;
|
||||||
if(data?.page === 'Error' && data?.statusText?.toLowerCase().includes('upstream')) {
|
if (
|
||||||
data.page = 'UpstreamError';
|
data?.page === "Error" &&
|
||||||
|
data?.statusText?.toLowerCase().includes("upstream")
|
||||||
|
) {
|
||||||
|
data.page = "UpstreamError";
|
||||||
}
|
}
|
||||||
switch (data?.page) {
|
switch (data?.page) {
|
||||||
case "UpstreamError":
|
case "UpstreamError":
|
||||||
|
@ -30,6 +34,9 @@ const App: FC = () => {
|
||||||
case "Error":
|
case "Error":
|
||||||
body = <ErrorPage data={data} />;
|
body = <ErrorPage data={data} />;
|
||||||
break;
|
break;
|
||||||
|
case "Routes":
|
||||||
|
body = <RoutesPage data={data} />;
|
||||||
|
break;
|
||||||
case "SignOutConfirm":
|
case "SignOutConfirm":
|
||||||
body = <SignOutConfirmPage data={data} />;
|
body = <SignOutConfirmPage data={data} />;
|
||||||
break;
|
break;
|
||||||
|
@ -64,7 +71,10 @@ const App: FC = () => {
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SubpageContextProvider page={data?.page}>
|
<SubpageContextProvider page={data?.page}>
|
||||||
<Header includeSidebar={data?.page === "UserInfo"} data={data} />
|
<Header
|
||||||
|
includeSidebar={data?.page === "UserInfo" || data?.page === "Routes"}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
<ToolbarOffset />
|
<ToolbarOffset />
|
||||||
<Box sx={{ overflow: "hidden", height: "calc(100vh - 120px)" }}>
|
<Box sx={{ overflow: "hidden", height: "calc(100vh - 120px)" }}>
|
||||||
<Box
|
<Box
|
||||||
|
|
|
@ -68,6 +68,11 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRoutes = (evt: React.MouseEvent): void => {
|
||||||
|
evt.preventDefault();
|
||||||
|
window.open("/.pomerium/routes");
|
||||||
|
};
|
||||||
|
|
||||||
const handleUserInfo = (evt: React.MouseEvent): void => {
|
const handleUserInfo = (evt: React.MouseEvent): void => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
window.open("/.pomerium/");
|
window.open("/.pomerium/");
|
||||||
|
@ -145,6 +150,7 @@ const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleUserInfo}>User Info</MenuItem>
|
<MenuItem onClick={handleUserInfo}>User Info</MenuItem>
|
||||||
|
<MenuItem onClick={handleRoutes}>Routes</MenuItem>
|
||||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
145
ui/src/components/RoutesPage.tsx
Normal file
145
ui/src/components/RoutesPage.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardActionArea,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Snackbar,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { Clipboard, Link } from "react-feather";
|
||||||
|
|
||||||
|
import { Route, RoutesPageData } from "../types";
|
||||||
|
import Section from "./Section";
|
||||||
|
import SidebarPage from "./SidebarPage";
|
||||||
|
|
||||||
|
type RouteCardProps = {
|
||||||
|
route: Route;
|
||||||
|
};
|
||||||
|
const RouteCard: FC<RouteCardProps> = ({ route }) => {
|
||||||
|
const [showSnackbar, setShowSnackbar] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = (evt: React.MouseEvent) => {
|
||||||
|
if (route.connect_command) {
|
||||||
|
evt.preventDefault();
|
||||||
|
navigator.clipboard.writeText(route.connect_command);
|
||||||
|
setShowSnackbar(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card raised={true}>
|
||||||
|
<CardActionArea href={route.from} target="_blank" onClick={handleClick}>
|
||||||
|
<CardHeader
|
||||||
|
avatar={
|
||||||
|
route.logo_url ? (
|
||||||
|
<Avatar src={route.logo_url} />
|
||||||
|
) : route.type === "tcp" ? (
|
||||||
|
<Avatar>TCP</Avatar>
|
||||||
|
) : route.type === "udp" ? (
|
||||||
|
<Avatar>UDP</Avatar>
|
||||||
|
) : (
|
||||||
|
<Avatar>
|
||||||
|
<Link />
|
||||||
|
</Avatar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
route.connect_command && (
|
||||||
|
<IconButton title="Copy Command">
|
||||||
|
<Clipboard />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={route.name}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
{route.description && (
|
||||||
|
<Typography variant="body2">{route.description}</Typography>
|
||||||
|
)}
|
||||||
|
{route.connect_command && (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ fontFamily: '"DM Mono"', fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
{route.connect_command}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
|
open={showSnackbar}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={() => setShowSnackbar(false)}
|
||||||
|
message="Copied to Clipboard"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoutesSectionProps = {
|
||||||
|
type: "http" | "tcp" | "udp";
|
||||||
|
title: string;
|
||||||
|
allRoutes: Route[];
|
||||||
|
};
|
||||||
|
const RoutesSection: FC<RoutesSectionProps> = ({ type, title, allRoutes }) => {
|
||||||
|
const routes = allRoutes?.filter((r) => r.type === type);
|
||||||
|
if (routes?.length === 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title={title}>
|
||||||
|
<Grid container spacing={2} justifyContent="center">
|
||||||
|
{routes?.map((r) => (
|
||||||
|
<Grid key={r.id} item sx={{ width: 300 }}>
|
||||||
|
<RouteCard route={r} />
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoutesPageProps = {
|
||||||
|
data: RoutesPageData;
|
||||||
|
};
|
||||||
|
const RoutesPage: FC<RoutesPageProps> = ({ data }) => {
|
||||||
|
return (
|
||||||
|
<SidebarPage>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{data?.routes ? (
|
||||||
|
<>
|
||||||
|
<RoutesSection
|
||||||
|
type={"http"}
|
||||||
|
title={"HTTP Routes"}
|
||||||
|
allRoutes={data.routes}
|
||||||
|
/>
|
||||||
|
<RoutesSection
|
||||||
|
type={"tcp"}
|
||||||
|
title={"TCP Routes"}
|
||||||
|
allRoutes={data.routes}
|
||||||
|
/>
|
||||||
|
<RoutesSection
|
||||||
|
type={"udp"}
|
||||||
|
title={"UDP Routes"}
|
||||||
|
allRoutes={data.routes}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Paper sx={{ padding: 3 }}>
|
||||||
|
<Typography>No accessible routes found</Typography>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</SidebarPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RoutesPage;
|
44
ui/src/components/SidebarPage.tsx
Normal file
44
ui/src/components/SidebarPage.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Box, Container, Drawer, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
import { ToolbarOffset } from "./ToolbarOffset";
|
||||||
|
import UserSidebarContent from "./UserSidebarContent";
|
||||||
|
|
||||||
|
const SidebarPage: FC = ({ children }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), {
|
||||||
|
defaultMatches: true,
|
||||||
|
noSsr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false}>
|
||||||
|
{mdUp && (
|
||||||
|
<Drawer
|
||||||
|
anchor="left"
|
||||||
|
open
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
backgroundColor: "neutral.900",
|
||||||
|
width: 256,
|
||||||
|
height: "100vh",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="persistent"
|
||||||
|
>
|
||||||
|
<ToolbarOffset />
|
||||||
|
<UserSidebarContent close={null} />
|
||||||
|
<ToolbarOffset />
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: mdUp ? "256px" : "0px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SidebarPage;
|
|
@ -1,15 +1,11 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Container,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Drawer,
|
|
||||||
Stack,
|
Stack,
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, { FC, useContext, useEffect, useState } from "react";
|
import React, { FC, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
@ -18,18 +14,12 @@ import { UserInfoData } from "../types";
|
||||||
import GroupDetails from "./GroupDetails";
|
import GroupDetails from "./GroupDetails";
|
||||||
import SessionDetails from "./SessionDetails";
|
import SessionDetails from "./SessionDetails";
|
||||||
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
||||||
import { ToolbarOffset } from "./ToolbarOffset";
|
import SidebarPage from "./SidebarPage";
|
||||||
import { UserSidebarContent } from "./UserSidebarContent";
|
|
||||||
|
|
||||||
type UserInfoPageProps = {
|
type UserInfoPageProps = {
|
||||||
data: UserInfoData & { page: "DeviceEnrolled" | "UserInfo" };
|
data: UserInfoData & { page: "DeviceEnrolled" | "UserInfo" };
|
||||||
};
|
};
|
||||||
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
const theme = useTheme();
|
|
||||||
const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), {
|
|
||||||
defaultMatches: true,
|
|
||||||
noSsr: false,
|
|
||||||
});
|
|
||||||
const { subpage } = useContext(SubpageContext);
|
const { subpage } = useContext(SubpageContext);
|
||||||
|
|
||||||
const [showDeviceEnrolled, setShowDeviceEnrolled] = useState(false);
|
const [showDeviceEnrolled, setShowDeviceEnrolled] = useState(false);
|
||||||
|
@ -47,7 +37,7 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth={false}>
|
<SidebarPage>
|
||||||
<Dialog open={showDeviceEnrolled} onClose={handleCloseDeviceEnrolled}>
|
<Dialog open={showDeviceEnrolled} onClose={handleCloseDeviceEnrolled}>
|
||||||
<DialogTitle>Device Enrolled</DialogTitle>
|
<DialogTitle>Device Enrolled</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@ -57,30 +47,7 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
<Button onClick={handleCloseDeviceEnrolled}>OK</Button>
|
<Button onClick={handleCloseDeviceEnrolled}>OK</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
{mdUp && (
|
<Stack spacing={3}>
|
||||||
<Drawer
|
|
||||||
anchor="left"
|
|
||||||
open
|
|
||||||
PaperProps={{
|
|
||||||
sx: {
|
|
||||||
backgroundColor: "neutral.900",
|
|
||||||
width: 256,
|
|
||||||
height: "100vh",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
variant="persistent"
|
|
||||||
>
|
|
||||||
<ToolbarOffset />
|
|
||||||
<UserSidebarContent close={null} />
|
|
||||||
<ToolbarOffset />
|
|
||||||
</Drawer>
|
|
||||||
)}
|
|
||||||
<Stack
|
|
||||||
spacing={3}
|
|
||||||
sx={{
|
|
||||||
marginLeft: mdUp ? "256px" : "0px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{subpage === "User" && (
|
{subpage === "User" && (
|
||||||
<SessionDetails session={data?.session} profile={data?.profile} />
|
<SessionDetails session={data?.session} profile={data?.profile} />
|
||||||
)}
|
)}
|
||||||
|
@ -103,7 +70,7 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</SidebarPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default UserInfoPage;
|
export default UserInfoPage;
|
||||||
|
|
|
@ -6,27 +6,36 @@ import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, { FC, ReactNode, useContext } from "react";
|
import React, { FC, ReactNode, useContext } from "react";
|
||||||
import { User, Users } from "react-feather";
|
import { Link, User, Users } from "react-feather";
|
||||||
|
|
||||||
import { SubpageContext } from "../context/Subpage";
|
import { SubpageContext } from "../context/Subpage";
|
||||||
|
|
||||||
export interface Subpage {
|
export interface Subpage {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
|
pathname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sectionList: Subpage[] = [
|
export const sectionList: Subpage[] = [
|
||||||
{
|
{
|
||||||
title: "User",
|
title: "User",
|
||||||
icon: <User />,
|
icon: <User />,
|
||||||
|
pathname: "/.pomerium/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Groups Info",
|
title: "Groups Info",
|
||||||
icon: <Users />,
|
icon: <Users />,
|
||||||
|
pathname: "/.pomerium/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Devices Info",
|
title: "Devices Info",
|
||||||
icon: <Devices />,
|
icon: <Devices />,
|
||||||
|
pathname: "/.pomerium/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Routes",
|
||||||
|
icon: <Link />,
|
||||||
|
pathname: "/.pomerium/routes",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
type UserSidebarContent = {
|
type UserSidebarContent = {
|
||||||
|
@ -39,12 +48,17 @@ export const UserSidebarContent: FC<UserSidebarContent> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
{sectionList.map(({ title, icon }) => {
|
{sectionList.map(({ title, icon, pathname }) => {
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={"tab " + title}
|
key={"tab " + title}
|
||||||
selected={title === info.subpage}
|
selected={title === info.subpage}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (location.pathname !== pathname) {
|
||||||
|
location.href =
|
||||||
|
pathname + "#subpage=" + encodeURIComponent(title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
info.setSubpage(title);
|
info.setSubpage(title);
|
||||||
!!close && close();
|
!!close && close();
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -18,11 +18,22 @@ export const SubpageContextProvider: FC<SubpageContextProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const setSubpage = (subpage: string) => {
|
const setSubpage = (subpage: string) => {
|
||||||
|
location.hash = "subpage=" + encodeURIComponent(subpage);
|
||||||
setState({ ...state, subpage });
|
setState({ ...state, subpage });
|
||||||
};
|
};
|
||||||
|
const hashParams = new URLSearchParams(location.hash.substring(1));
|
||||||
|
|
||||||
const initState = {
|
const initState = {
|
||||||
subpage: page === "DeviceEnrolled" ? "Devices Info" : "User",
|
subpage:
|
||||||
|
page === "DeviceEnrolled"
|
||||||
|
? "Devices Info"
|
||||||
|
: page === "Routes"
|
||||||
|
? "Routes"
|
||||||
|
: hashParams.get("subpage") === "Groups Info"
|
||||||
|
? "Groups Info"
|
||||||
|
: hashParams.get("subpage") === "Devices Info"
|
||||||
|
? "Devices Info"
|
||||||
|
: "User",
|
||||||
setSubpage,
|
setSubpage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,22 @@ export type DeviceEnrolledPageData = BasePageData &
|
||||||
page: "DeviceEnrolled";
|
page: "DeviceEnrolled";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "http" | "tcp" | "udp";
|
||||||
|
from: string;
|
||||||
|
connect_command?: string;
|
||||||
|
description: string;
|
||||||
|
logo_url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RoutesPageData = BasePageData &
|
||||||
|
UserInfoData & {
|
||||||
|
page: "Routes";
|
||||||
|
routes: Route[];
|
||||||
|
};
|
||||||
|
|
||||||
export type SignOutConfirmPageData = BasePageData & {
|
export type SignOutConfirmPageData = BasePageData & {
|
||||||
page: "SignOutConfirm";
|
page: "SignOutConfirm";
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -135,6 +151,7 @@ export type WebAuthnRegistrationPageData = BasePageData & {
|
||||||
export type PageData =
|
export type PageData =
|
||||||
| ErrorPageData
|
| ErrorPageData
|
||||||
| DeviceEnrolledPageData
|
| DeviceEnrolledPageData
|
||||||
|
| RoutesPageData
|
||||||
| SignOutConfirmPageData
|
| SignOutConfirmPageData
|
||||||
| SignedOutPageData
|
| SignedOutPageData
|
||||||
| UserInfoPageData
|
| UserInfoPageData
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue