Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move login to menu content #1056

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
};