Skip to content

Commit

Permalink
Merge pull request #1056 from amvanbaren/webui-readonly
Browse files Browse the repository at this point in the history
Move login to menu content
  • Loading branch information
amvanbaren authored Dec 9, 2024
2 parents 37a929d + b59dc99 commit 8bffeb0
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 76 deletions.
2 changes: 1 addition & 1 deletion webui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openvsx-webui",
"version": "0.13.1",
"version": "0.13.1-next.dee19c0f",
"description": "User interface for Eclipse Open VSX",
"keywords": [
"react",
Expand Down
139 changes: 118 additions & 21 deletions webui/src/default/menu-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,143 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/

import React, { FunctionComponent, PropsWithChildren } from 'react';
import { Typography, MenuItem, Link, Button } from '@mui/material';
import React, { FunctionComponent, PropsWithChildren, useContext } from 'react';
import { Typography, MenuItem, Link, Button, IconButton, Accordion, AccordionSummary, Avatar, AccordionDetails } from '@mui/material';
import { useLocation } from 'react-router-dom';
import { Link as RouteLink } from 'react-router-dom';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import ForumIcon from '@mui/icons-material/Forum';
import InfoIcon from '@mui/icons-material/Info';
import PublishIcon from '@mui/icons-material/Publish';
import AccountBoxIcon from '@mui/icons-material/AccountBox';
import { UserAvatar } from '../pages/user/avatar';
import { UserSettingsRoutes } from '../pages/user/user-settings';
import { styled, Theme } from '@mui/material/styles';
import { MainContext } from '../context';
import SettingsIcon from '@mui/icons-material/Settings';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import LogoutIcon from '@mui/icons-material/Logout';
import { AdminDashboardRoutes } from '../pages/admin-dashboard/admin-dashboard';
import { LogoutForm } from '../pages/user/logout';

//-------------------- Mobile View --------------------//

const MobileMenuItem = styled(MenuItem)({
export const MobileMenuItem = styled(MenuItem)({
cursor: 'auto',
'&>a': {
textDecoration: 'none'
}
});

const itemIcon = {
export const itemIcon = {
mr: 1,
width: '16px',
height: '16px',
};

const MobileMenuItemText: FunctionComponent<PropsWithChildren> = ({ children }) => {
export const MobileMenuItemText: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<Typography variant='body2' color='text.primary' sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant='body2' color='text.primary' sx={{ display: 'flex', alignItems: 'center', textTransform: 'none' }}>
{children}
</Typography>
);
};

export const MobileUserAvatar: FunctionComponent = () => {
const context = useContext(MainContext);
const user = context.user;
if (!user) {
return null;
}

return <Accordion sx={{ border: 0, borderRadius: 0, boxShadow: '0 0', background: 'transparent' }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='user-actions'
id='user-avatar'
>
<MobileMenuItemText>
<Avatar
src={user.avatarUrl}
alt={user.loginName}
variant='rounded'
sx={itemIcon} />
{user.loginName}
</MobileMenuItemText>
</AccordionSummary>
<AccordionDetails>
<MobileMenuItem>
<Link href={user.homepage}>
<MobileMenuItemText>
<GitHubIcon sx={itemIcon} />
{user.loginName}
</MobileMenuItemText>
</Link>
</MobileMenuItem>
<MobileMenuItem>
<RouteLink to={UserSettingsRoutes.PROFILE}>
<MobileMenuItemText>
<SettingsIcon sx={itemIcon} />
Settings
</MobileMenuItemText>
</RouteLink>
</MobileMenuItem>
{
user.role === 'admin'
? <MobileMenuItem>
<RouteLink to={AdminDashboardRoutes.MAIN}>
<MobileMenuItemText>
<AdminPanelSettingsIcon sx={itemIcon} />
Admin Dashboard
</MobileMenuItemText>
</RouteLink>
</MobileMenuItem>
: null
}
<MobileMenuItem>
<LogoutForm>
<MobileMenuItemText>
<LogoutIcon sx={itemIcon} />
Log Out
</MobileMenuItemText>
</LogoutForm>
</MobileMenuItem>
</AccordionDetails>
</Accordion>;
};

export const MobileMenuContent: FunctionComponent = () => {

const location = useLocation();
const { service, user } = useContext(MainContext);

return <>
{
user
? <MobileUserAvatar/>
: <MobileMenuItem>
<Link href={service.getLoginUrl()}>
<MobileMenuItemText>
<AccountBoxIcon sx={itemIcon} />
Log In
</MobileMenuItemText>
</Link>
</MobileMenuItem>
}
{
!location.pathname.startsWith(UserSettingsRoutes.ROOT)
? <MobileMenuItem>
<RouteLink to='/user-settings/extensions'>
<MobileMenuItemText>
<PublishIcon sx={itemIcon} />
Publish Extension
</MobileMenuItemText>
</RouteLink>
</MobileMenuItem>
: null
}
<MobileMenuItem>
<Link target='_blank' href='https://github.com/eclipse/openvsx'>
<MobileMenuItemText>
Expand Down Expand Up @@ -80,24 +177,12 @@ export const MobileMenuContent: FunctionComponent = () => {
</MobileMenuItemText>
</RouteLink>
</MobileMenuItem>
{
!location.pathname.startsWith(UserSettingsRoutes.ROOT)
? <MobileMenuItem>
<RouteLink to='/user-settings/extensions'>
<MobileMenuItemText>
<PublishIcon sx={itemIcon} />
Publish Extension
</MobileMenuItemText>
</RouteLink>
</MobileMenuItem>
: null
}
</>;
};

//-------------------- Default View --------------------//

const headerItem = ({ theme }: { theme: Theme }) => ({
export const headerItem = ({ theme }: { theme: Theme }) => ({
margin: theme.spacing(2.5),
color: theme.palette.text.primary,
textDecoration: 'none',
Expand All @@ -111,10 +196,11 @@ const headerItem = ({ theme }: { theme: Theme }) => ({
}
});

const MenuLink = styled(Link)(headerItem);
const MenuRouteLink = styled(RouteLink)(headerItem);
export const MenuLink = styled(Link)(headerItem);
export const MenuRouteLink = styled(RouteLink)(headerItem);

export const DefaultMenuContent: FunctionComponent = () => {
const { service, user } = useContext(MainContext);
return <>
<MenuLink href='https://github.com/eclipse/openvsx/wiki'>
Documentation
Expand All @@ -128,5 +214,16 @@ export const DefaultMenuContent: FunctionComponent = () => {
<Button variant='contained' color='secondary' href='/user-settings/extensions' sx={{ mx: 2.5 }}>
Publish
</Button>
{
user ?
<UserAvatar />
:
<IconButton
href={service.getLoginUrl()}
title='Log In'
aria-label='Log In' >
<AccountBoxIcon />
</IconButton>
}
</>;
};
1 change: 1 addition & 0 deletions webui/src/header-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const MobileHeaderMenu: FunctionComponent<MobileHeaderMenuProps> = props
<Menu
open={open}
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
onClose={() => setOpen(false)} >
<MenuContent />
Expand Down
17 changes: 2 additions & 15 deletions webui/src/other-pages.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { FunctionComponent, useContext, useEffect, useState } from 'react';
import { Routes, Route } from 'react-router-dom';
import { AppBar, Box, IconButton, Toolbar } from '@mui/material';
import { AppBar, Box, Toolbar } from '@mui/material';
import { styled, Theme } from '@mui/material/styles';
import AccountBoxIcon from '@mui/icons-material/AccountBox';
import { Banner } from './components/banner';
import { MainContext } from './context';
import { HeaderMenu } from './header-menu';
import { UserAvatar } from './pages/user/avatar';
import { ExtensionListContainer, ExtensionListRoutes } from './pages/extension-list/extension-list-container';
import { UserSettings, UserSettingsRoutes } from './pages/user/user-settings';
import { NamespaceDetail, NamespaceDetailRoutes } from './pages/namespace-detail/namespace-detail';
Expand Down Expand Up @@ -38,7 +36,7 @@ const Footer = styled('footer')(({ theme }: { theme: Theme }) => ({
}));

export const OtherPages: FunctionComponent<OtherPagesProps> = (props) => {
const { service, pageSettings } = useContext(MainContext);
const { pageSettings } = useContext(MainContext);
const {
additionalRoutes: AdditionalRoutes,
banner: BannerComponent,
Expand Down Expand Up @@ -89,17 +87,6 @@ export const OtherPages: FunctionComponent<OtherPagesProps> = (props) => {
</ToolbarItem>
<ToolbarItem>
<HeaderMenu />
{
props.user ?
<UserAvatar />
:
<IconButton
href={service.getLoginUrl()}
title='Log In'
aria-label='Log In' >
<AccountBoxIcon />
</IconButton>
}
</ToolbarItem>
</Toolbar>
</AppBar>
Expand Down
50 changes: 11 additions & 39 deletions webui/src/pages/user/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,28 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/

import React, { FunctionComponent, useContext, useEffect, useRef, useState } from 'react';
import React, { FunctionComponent, useContext, useRef, useState } from 'react';
import { styled } from '@mui/material/styles';
import { Avatar, Button, Menu, Typography, MenuItem, Link, Divider, IconButton } from '@mui/material';
import { Avatar, Menu, Typography, MenuItem, Link, Divider, IconButton } from '@mui/material';
import { Link as RouteLink } from 'react-router-dom';
import { isError, CsrfTokenJson } from '../../extension-registry-types';
import { UserSettingsRoutes } from './user-settings';
import { AdminDashboardRoutes } from '../admin-dashboard/admin-dashboard';
import { MainContext } from '../../context';
import { LogoutForm } from './logout';

const link = {
const AvatarRouteLink = styled(RouteLink)({
cursor: 'pointer',
textDecoration: 'none'
};
});

const AvatarRouteLink = styled(RouteLink)(link);
const AvatarMenuItem = styled(MenuItem)({ cursor: 'auto' });
const LogoutButton = styled(Button)({
...link,
border: 'none',
background: 'none',
padding: 0
});


export const UserAvatar: FunctionComponent = () => {
const [open, setOpen] = useState<boolean>(false);
const [csrf, setCsrf] = useState<string>();
const context = useContext(MainContext);
const avatarButton = useRef<any>();

const abortController = useRef<AbortController>(new AbortController());
useEffect(() => {
updateCsrf();
return () => abortController.current.abort();
}, []);

const updateCsrf = async () => {
try {
const csrfResponse = await context.service.getCsrfToken(abortController.current);
if (!isError(csrfResponse)) {
const csrfToken = csrfResponse as CsrfTokenJson;
setCsrf(csrfToken.value);
}
} catch (err) {
context.handleError(err);
}
};

const handleAvatarClick = () => {
setOpen(!open);
};
Expand Down Expand Up @@ -116,14 +91,11 @@ export const UserAvatar: FunctionComponent = () => {
''
}
<AvatarMenuItem>
<form method='post' action={context.service.getLogoutUrl()}>
{csrf ? <input name='_csrf' type='hidden' value={csrf} /> : null}
<LogoutButton type='submit'>
<Typography variant='button' sx={{ color: 'primary.dark' }}>
Log Out
</Typography>
</LogoutButton>
</form>
<LogoutForm>
<Typography variant='button' sx={{ color: 'primary.dark' }}>
Log Out
</Typography>
</LogoutForm>
</AvatarMenuItem>
</Menu>
</>;
Expand Down
53 changes: 53 additions & 0 deletions webui/src/pages/user/logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** ******************************************************************************
* Copyright (c) 2024 Precies. Software OU and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
* ****************************************************************************** */

import React, { FunctionComponent, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
import { Button } from '@mui/material';
import { styled } from '@mui/material/styles';
import { isError, CsrfTokenJson } from '../../extension-registry-types';
import { MainContext } from '../../context';

const LogoutButton = styled(Button)({
cursor: 'pointer',
textDecoration: 'none',
border: 'none',
background: 'none',
padding: 0
});

export const LogoutForm: FunctionComponent<PropsWithChildren> = ({ children }) => {
const [csrf, setCsrf] = useState<string>();
const context = useContext(MainContext);

const abortController = useRef<AbortController>(new AbortController());
useEffect(() => {
updateCsrf();
return () => abortController.current.abort();
}, []);

const updateCsrf = async () => {
try {
const csrfResponse = await context.service.getCsrfToken(abortController.current);
if (!isError(csrfResponse)) {
const csrfToken = csrfResponse as CsrfTokenJson;
setCsrf(csrfToken.value);
}
} catch (err) {
context.handleError(err);
}
};

return <form method='post' action={context.service.getLogoutUrl()}>
{csrf ? <input name='_csrf' type='hidden' value={csrf} /> : null}
<LogoutButton type='submit'>
{children}
</LogoutButton>
</form>;
};

0 comments on commit 8bffeb0

Please sign in to comment.