feat: rework web header for mobile, add new settings and raw manifest button (#3129)

* feat: rework header, dialogs and new raw mode

* chore: add test for raw button and hide download tarball

* chore: add test hide footer

* chore: add docs to config files

* chore: add changeset

* chore: enable raw by default
This commit is contained in:
Juan Picado 2022-04-16 12:26:02 +02:00 committed by GitHub
parent 8ea712935e
commit d43894e8f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2736 additions and 830 deletions

View File

@ -0,0 +1,43 @@
---
'@verdaccio/config': minor
'@verdaccio/types': minor
'@verdaccio/ui-theme': minor
---
feat: rework web header for mobile, add new settings and raw manifest button
### New set of variables to hide features
Add set of new variables that allow hide different parts of the UI, buttons, footer or download tarballs. _All are
enabled by default_.
```yaml
# login: true <-- already exist but worth the reminder
# showInfo: true
# showSettings: true
# In combination with darkMode you can force specific theme
# showThemeSwitch: true
# showFooter: true
# showSearch: true
# showDownloadTarball: true
```
> If you disable `showThemeSwitch` and force `darkMode: true` the local storage settings would be
> ignored and force all themes to the one in the configuration file.
Future could be extended to
### Raw button to display manifest package
A new experimental feature (enabled by default), button named RAW to be able navigate on the package manifest directly on the ui, kudos to [react-json-view](https://www.npmjs.com/package/react-json-view) that allows an easy integration, not configurable yet until get more feedback.
```yaml
showRaw: true
```
#### Rework header buttons
- The header has been rework, the mobile was not looking broken.
- Removed info button in the header and moved to a dialog
- Info dialog now contains more information about the project, license and the aid content for Ukrania now is inside of the info modal.
- Separate settings and info to avoid collapse too much info (for mobile still need some work)

View File

@ -20,6 +20,16 @@ web:
# convert your UI to the dark side
# darkMode: true
# html_cache: true
# by default all features are displayed
# login: true
# showInfo: true
# showSettings: true
# In combination with darkMode you can force specific theme
# showThemeSwitch: true
# showFooter: true
# showSearch: true
# showRaw: true
# showDownloadTarball: true
# HTML tags injected after manifest <scripts/>
# scriptsBodyAfter:
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'

View File

@ -26,6 +26,16 @@ web:
# convert your UI to the dark side
# darkMode: true
# html_cache: true
# by default all features are displayed
# login: true
# showInfo: true
# showSettings: true
# In combination with darkMode you can force specific theme
# showThemeSwitch: true
# showFooter: true
# showSearch: true
# showRaw: true
# showDownloadTarball: true
# HTML tags injected after manifest <scripts/>
# scriptsBodyAfter:
# - '<script type="text/javascript" src="https://my.company.com/customJS.min.js"></script>'

View File

@ -43,6 +43,13 @@ declare module '@verdaccio/types' {
// deprecated
basename?: string;
scope?: string;
showInfo?: boolean;
showSettings?: boolean;
showSearch?: boolean;
showFooter?: boolean;
showThemeSwitch?: boolean;
showDownloadTarball?: boolean;
showRaw?: boolean;
base: string;
primaryColor?: string;
version?: string;

View File

@ -30,6 +30,7 @@ module.exports = Object.assign({}, config, {
'\\.(png)$': '<rootDir>/jest/identity.js',
'\\.(svg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(jpg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(md)$': '<rootDir>/jest/unit/empty-string.ts',
'github-markdown-css': '<rootDir>/jest/identity.js',
// note: this section has to be on sync with webpack configuration
'verdaccio-ui/components/(.*)': '<rootDir>/src/components/$1',

View File

@ -0,0 +1 @@
export default 'empty string module';

View File

@ -57,6 +57,7 @@
"node-mocks-http": "1.11.0",
"normalize.css": "8.0.1",
"react-markdown": "8.0.0",
"react-json-view": "1.21.3",
"remark-gfm": "3.0.1",
"optimize-css-assets-webpack-plugin": "6.0.1",
"ora": "5.4.1",
@ -71,6 +72,7 @@
"react-redux": "7.2.6",
"redux": "4.1.2",
"rimraf": "3.0.2",
"raw-loader": "4.0.2",
"msw": "0.36.5",
"style-loader": "3.3.1",
"stylelint": "14.6.0",

View File

@ -118,4 +118,19 @@ describe('<App />', () => {
expect(store.getState().packages.response).toHaveLength(1);
}, 10000);
});
describe('footer', () => {
test('should display the Header component', () => {
renderWithStore(<App />, store);
expect(screen.getByTestId('footer')).toBeInTheDocument();
});
test('should not display the Header component', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
showFooter: false,
};
renderWithStore(<App />, store);
expect(screen.queryByTestId('footer')).toBeFalsy();
});
});
});

View File

@ -7,6 +7,7 @@ import Loading from 'verdaccio-ui/components/Loading';
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
import loadDayJSLocale from 'verdaccio-ui/design-tokens/load-dayjs-locale';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
import '../i18n/config';
import AppRoute, { history } from './AppRoute';
@ -27,10 +28,11 @@ const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
}));
const App: React.FC = () => {
const { configOptions } = useConfig();
useEffect(() => {
loadDayJSLocale();
}, []);
return (
<Suspense fallback={<Loading />}>
<StyleBaseline />
@ -42,7 +44,7 @@ const App: React.FC = () => {
<AppRoute />
</StyledBoxContent>
</Router>
<Footer />
{configOptions.showFooter && <Footer />}
</>
</StyledBox>
</Suspense>

View File

@ -18,7 +18,7 @@ const Footer = () => {
const { t } = useTranslation();
const { configOptions } = useConfig();
return (
<Wrapper>
<Wrapper data-testid="footer">
<Inner>
<Left>
<Trans components={[<Love />]} i18nKey="footer.made-with-love-on" />

View File

@ -165,6 +165,7 @@ exports[`<Footer /> component should load the initial state of Footer component
<div
class="emotion-0 emotion-1"
data-testid="footer"
>
<div
class="emotion-2 emotion-3"

View File

@ -0,0 +1,28 @@
import Avatar from '@mui/material/Avatar';
import AvatarGroup from '@mui/material/AvatarGroup';
import Link from '@mui/material/Link';
import React from 'react';
import contributors from './generated_contributors_list.json';
const generateImage = (id) => `https://avatars3.githubusercontent.com/u/${id}?s=120&v=4`;
const Contributors: React.FC = () => {
return (
<>
<Link href={`https://verdaccio.org/contributors`} rel="noreferrer" target="_blank">
<AvatarGroup max={18} spacing={15} total={400}>
{contributors?.map(({ username, id }) => {
return (
<div key={username}>
<Avatar alt={username} src={generateImage(id)} />
</div>
);
})}
</AvatarGroup>
</Link>
</>
);
};
export default Contributors;

View File

@ -80,33 +80,61 @@ describe('<Header /> component with logged in state', () => {
expect(getByText('Login')).toBeTruthy();
});
test("The question icon should open a new tab of verdaccio's website - installation doc", () => {
const { getByTestId } = renderWithStore(
test('should display info button', () => {
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.getByTestId('header--tooltip-info')).toBeInTheDocument();
});
const documentationBtn = getByTestId('header--tooltip-documentation');
expect(documentationBtn.getAttribute('href')).toBe(
'https://verdaccio.org/docs/en/installation'
test('should display settings button', () => {
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.getByTestId('header--tooltip-settings')).toBeInTheDocument();
});
test('should display light button switch', () => {
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.getByTestId('header--button--light')).toBeInTheDocument();
});
test.todo('should test display dark button switch');
test('should display search box', () => {
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.getByTestId('search-container')).toBeInTheDocument();
});
test('should open the registrationInfo modal when clicking on the info icon', async () => {
const { getByTestId } = renderWithStore(
renderWithStore(
<Router>
<Header />
</Router>,
store
);
const infoBtn = getByTestId('header--tooltip-info');
const infoBtn = screen.getByTestId('header--tooltip-info');
expect(infoBtn).toBeInTheDocument();
fireEvent.click(infoBtn);
// wait for registrationInfo modal appearance and return the element
const registrationInfoModal = await waitFor(() => getByTestId('registryInfo--dialog'));
const registrationInfoModal = await waitFor(() => screen.getByTestId('registryInfo--dialog'));
expect(registrationInfoModal).toBeTruthy();
});
@ -143,7 +171,67 @@ describe('<Header /> component with logged in state', () => {
store
);
expect(screen.queryByTestId('header--button-login')).not.toBeInTheDocument();
expect(screen.queryByTestId('header--button-login')).toBeNull();
});
test('should hide search if is disabled', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
showSearch: false,
};
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.queryByTestId('search-container')).toBeNull();
});
test('should hide settings if is disabled', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
showSettings: false,
};
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.queryByTitle('header--tooltip-settings')).toBeNull();
});
test('should hide info if is disabled', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
showSettings: false,
};
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.queryByTitle('header.registry-info')).toBeNull();
});
test('should hide theme switch if is disabled', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'foo',
showThemeSwitch: false,
};
renderWithStore(
<Router>
<Header />
</Router>,
store
);
expect(screen.queryByTitle('header.registry-info')).toBeNull();
});
test.todo('autocompletion should display suggestions according to the type value');

View File

@ -8,18 +8,16 @@ import { Dispatch, RootState } from '../../store/store';
import HeaderInfoDialog from './HeaderInfoDialog';
import HeaderLeft from './HeaderLeft';
import HeaderRight from './HeaderRight';
import HeaderSettingsDialog from './HeaderSettingsDialog';
import LoginDialog from './LoginDialog';
import Search from './Search';
import { InnerMobileNavBar, InnerNavBar, MobileNavBar, NavBar } from './styles';
interface Props {
withoutSearch?: boolean;
}
/* eslint-disable react/jsx-no-bind*/
const Header: React.FC<Props> = ({ withoutSearch }) => {
const Header: React.FC = () => {
const { t } = useTranslation();
const [isInfoDialogOpen, setOpenInfoDialog] = useState<boolean>(false);
const [isSettingsDialogOpen, setSettingsDialogOpen] = useState<boolean>(false);
const [showMobileNavBar, setShowMobileNavBar] = useState<boolean>(false);
const [showLoginModal, setShowLoginModal] = useState<boolean>(false);
const loginStore = useSelector((state: RootState) => state.login);
@ -28,30 +26,35 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
const handleLogout = () => {
dispatch.login.logOutUser();
};
return (
<>
<NavBar data-testid="header" position="static">
<InnerNavBar>
<HeaderLeft />
<HeaderLeft showSearch={configOptions.showSearch} />
<HeaderRight
hasLogin={configOptions?.login}
onLogout={handleLogout}
onOpenRegistryInfoDialog={() => setOpenInfoDialog(true)}
onOpenSettingsDialog={() => setSettingsDialogOpen(true)}
onToggleLogin={() => setShowLoginModal(!showLoginModal)}
onToggleMobileNav={() => setShowMobileNavBar(!showMobileNavBar)}
showInfo={configOptions.showInfo}
showSearch={configOptions.showSearch}
showSettings={configOptions.showSettings}
showThemeSwitch={configOptions.showThemeSwitch}
username={loginStore?.username}
withoutSearch={withoutSearch}
/>
</InnerNavBar>
{
<HeaderInfoDialog
isOpen={isInfoDialogOpen}
onCloseDialog={() => setOpenInfoDialog(false)}
/>
}
<HeaderSettingsDialog
isOpen={isSettingsDialogOpen}
onCloseDialog={() => setSettingsDialogOpen(false)}
/>
<HeaderInfoDialog
isOpen={isInfoDialogOpen}
onCloseDialog={() => setOpenInfoDialog(false)}
/>
</NavBar>
{showMobileNavBar && !withoutSearch && (
{showMobileNavBar && (
<MobileNavBar>
<InnerMobileNavBar>
<Search />

View File

@ -1,19 +1,22 @@
/* eslint-disable react/jsx-pascal-case */
/* eslint-disable verdaccio/jsx-spread */
import styled from '@emotion/styled';
import { Theme } from '@mui/material';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import FlagsIcon from 'country-flag-icons/react/3x2';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
import remarkGfm from 'remark-gfm';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { RootState } from '../../store/store';
import LanguageSwitch from './LanguageSwitch';
import RegistryInfoContent from './RegistryInfoContent';
import Contributors from './Contributors';
import RegistryInfoDialog from './RegistryInfoDialog';
import { Support } from './Support';
import about from './about.md';
import license from './license.md';
interface Props {
isOpen: boolean;
@ -43,36 +46,47 @@ function TabPanel(props) {
);
}
const TextContent = styled('div')<{ theme?: Theme }>(({ theme }) => ({
padding: '10px 0',
backgroundColor: theme?.palette.background.default,
const Flags = styled('span')<{ theme?: Theme }>(() => ({
width: '25px',
}));
const HeaderInfoDialog: React.FC<Props> = ({ onCloseDialog, isOpen }) => {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
const handleChange = (_event, newValue) => {
setValue(newValue);
};
const configStore = useSelector((state: RootState) => state.configuration.config);
const { scope, base } = configStore;
const { t } = useTranslation();
return (
<RegistryInfoDialog onClose={onCloseDialog} open={isOpen}>
<RegistryInfoDialog
onClose={onCloseDialog}
open={isOpen}
title={t('dialog.registry-info.title')}
>
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs aria-label="basic tabs example" onChange={handleChange} value={value}>
<Tab label={t('packageManagers.title')} {...a11yProps(0)} />
<Tab label={t('language.title')} {...a11yProps(1)} />
<Tab label={t('about')} {...a11yProps(0)} />
<Tab label={t('dialog.license')} {...a11yProps(1)} />
<Tab
{...a11yProps(2)}
icon={
<Flags>
<FlagsIcon.UA />
</Flags>
}
/>
</Tabs>
</Box>
<TabPanel index={0} value={value}>
<RegistryInfoContent registryUrl={base} scope={scope} />
<ReactMarkdown remarkPlugins={[remarkGfm]}>{about}</ReactMarkdown>
<Contributors />
</TabPanel>
<TabPanel index={1} value={value}>
<TextContent>{t('language.description')}</TextContent>
<LanguageSwitch />
<ReactMarkdown remarkPlugins={[remarkGfm]}>{t('language.contribute')}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{license}</ReactMarkdown>
</TabPanel>
<TabPanel index={2} value={value}>
<Support />
</TabPanel>
</Box>
</RegistryInfoDialog>

View File

@ -7,20 +7,20 @@ import Search from './Search';
import { LeftSide, SearchWrapper } from './styles';
interface Props {
withoutSearch?: boolean;
showSearch?: boolean;
}
const StyledLink = styled(Link)({
marginRight: '1em',
});
const HeaderLeft: React.FC<Props> = ({ withoutSearch = false }) => (
const HeaderLeft: React.FC<Props> = ({ showSearch }) => (
<LeftSide>
<StyledLink to={'/'}>
<Logo />
</StyledLink>
{!withoutSearch && (
<SearchWrapper>
{showSearch && (
<SearchWrapper data-testid="search-container">
<Search />
</SearchWrapper>
)}

View File

@ -5,27 +5,34 @@ import ThemeContext from 'verdaccio-ui/design-tokens/ThemeContext';
import HeaderMenu from './HeaderMenu';
import HeaderToolTip from './HeaderToolTip';
import { Support } from './Support';
import { RightSide } from './styles';
interface Props {
withoutSearch?: boolean;
showSearch?: boolean;
username?: string | null;
hasLogin?: boolean;
showInfo?: boolean;
showSettings?: boolean;
showThemeSwitch?: boolean;
onToggleLogin: () => void;
onOpenRegistryInfoDialog: () => void;
onOpenSettingsDialog: () => void;
onToggleMobileNav: () => void;
onLogout: () => void;
}
const HeaderRight: React.FC<Props> = ({
withoutSearch = false,
showSearch,
username,
onToggleLogin,
hasLogin,
showInfo,
showSettings,
showThemeSwitch,
onLogout,
onToggleMobileNav,
onOpenRegistryInfoDialog,
onOpenSettingsDialog,
}) => {
const themeContext = useContext(ThemeContext);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
@ -72,25 +79,35 @@ const HeaderRight: React.FC<Props> = ({
return (
<RightSide data-testid="header-right">
{!withoutSearch && (
{showSearch === true && (
<HeaderToolTip
onClick={onToggleMobileNav}
title={t('search.packages')}
tooltipIconType={'search'}
/>
)}
<Support />
<HeaderToolTip title={t('header.documentation')} tooltipIconType={'help'} />
<HeaderToolTip
onClick={onOpenRegistryInfoDialog}
title={t('header.registry-info')}
tooltipIconType={'info'}
/>
<HeaderToolTip
onClick={handleToggleDarkLightMode}
title={t('header.documentation')}
tooltipIconType={themeContext.isDarkMode ? 'dark-mode' : 'light-mode'}
/>
{showSettings === true && (
<HeaderToolTip
onClick={onOpenSettingsDialog}
title={t('header.settings')}
tooltipIconType={'settings'}
/>
)}
{showInfo === true && (
<HeaderToolTip
onClick={onOpenRegistryInfoDialog}
title={t('header.registry-info')}
tooltipIconType={'info'}
/>
)}
{showThemeSwitch === true && (
<HeaderToolTip
onClick={handleToggleDarkLightMode}
title={t('header.documentation')}
tooltipIconType={themeContext.isDarkMode ? 'dark-mode' : 'light-mode'}
/>
)}
{!hideLoginSection && (
<>

View File

@ -0,0 +1,82 @@
/* eslint-disable verdaccio/jsx-spread */
import styled from '@emotion/styled';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
import remarkGfm from 'remark-gfm';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { RootState } from '../../store/store';
import LanguageSwitch from './LanguageSwitch';
import RegistryInfoContent from './RegistryInfoContent';
import RegistryInfoDialog from './RegistryInfoDialog';
interface Props {
isOpen: boolean;
onCloseDialog: () => void;
}
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
aria-labelledby={`simple-tab-${index}`}
hidden={value !== index}
id={`simple-tabpanel-${index}`}
role="tabpanel"
{...other}
>
{value === index && <Box sx={{ paddingTop: 3 }}>{children}</Box>}
</div>
);
}
const TextContent = styled('div')<{ theme?: Theme }>(({ theme }) => ({
padding: '10px 0',
backgroundColor: theme?.palette.background.default,
}));
const HeaderSettingsDialog: React.FC<Props> = ({ onCloseDialog, isOpen }) => {
const [value, setValue] = React.useState(0);
const handleChange = (_event, newValue) => {
setValue(newValue);
};
const configStore = useSelector((state: RootState) => state.configuration.config);
const { scope, base } = configStore;
const { t } = useTranslation();
return (
<RegistryInfoDialog onClose={onCloseDialog} open={isOpen} title={t('dialog.settings.title')}>
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs aria-label="basic tabs example" onChange={handleChange} value={value}>
<Tab label={t('packageManagers.title')} {...a11yProps(0)} />
<Tab label={t('language.title')} {...a11yProps(1)} />
</Tabs>
</Box>
<TabPanel index={0} value={value}>
<RegistryInfoContent registryUrl={base} scope={scope} />
</TabPanel>
<TabPanel index={1} value={value}>
<TextContent>{t('language.description')}</TextContent>
<LanguageSwitch />
<ReactMarkdown remarkPlugins={[remarkGfm]}>{t('language.contribute')}</ReactMarkdown>
</TabPanel>
</Box>
</RegistryInfoDialog>
);
};
export default HeaderSettingsDialog;

View File

@ -1,14 +1,13 @@
import Help from '@mui/icons-material/Help';
import Info from '@mui/icons-material/Info';
import NightsStay from '@mui/icons-material/NightsStay';
import Search from '@mui/icons-material/Search';
import Settings from '@mui/icons-material/Settings';
import WbSunny from '@mui/icons-material/WbSunny';
import IconButton from '@mui/material/IconButton';
import React, { forwardRef } from 'react';
import { IconSearchButton, StyledLink } from './styles';
import { IconSearchButton, InfoButton, SettingsButtom, SwitchThemeButton } from './styles';
export type TooltipIconType = 'search' | 'help' | 'info' | 'dark-mode' | 'light-mode';
export type TooltipIconType = 'search' | 'info' | 'dark-mode' | 'light-mode' | 'settings';
interface Props {
tooltipIconType: TooltipIconType;
onClick?: () => void;
@ -23,21 +22,9 @@ const HeaderToolTipIcon = forwardRef<HeaderToolTipIconRef, Props>(function Heade
ref
) {
switch (tooltipIconType) {
case 'help':
return (
<StyledLink
data-testid={'header--tooltip-documentation'}
external={true}
to={'https://verdaccio.org/docs/en/installation'}
>
<IconButton color={'inherit'} size="large">
<Help />
</IconButton>
</StyledLink>
);
case 'info':
return (
<IconButton
<InfoButton
color="inherit"
data-testid={'header--tooltip-info'}
id="header--button-registryInfo"
@ -46,7 +33,20 @@ const HeaderToolTipIcon = forwardRef<HeaderToolTipIconRef, Props>(function Heade
size="large"
>
<Info />
</IconButton>
</InfoButton>
);
case 'settings':
return (
<SettingsButtom
color="inherit"
data-testid={'header--tooltip-settings'}
id="header--button-settings"
onClick={onClick}
ref={ref}
size="large"
>
<Settings />
</SettingsButtom>
);
case 'search':
return (
@ -56,16 +56,28 @@ const HeaderToolTipIcon = forwardRef<HeaderToolTipIconRef, Props>(function Heade
);
case 'dark-mode':
return (
<IconButton color="inherit" onClick={onClick} ref={ref} size="large">
<SwitchThemeButton
color="inherit"
data-testid={'header--button--dark'}
onClick={onClick}
ref={ref}
size="large"
>
<NightsStay />
</IconButton>
</SwitchThemeButton>
);
case 'light-mode':
return (
<IconButton color="inherit" onClick={onClick} ref={ref} size="large">
<SwitchThemeButton
color="inherit"
data-testid={'header--button--light'}
onClick={onClick}
ref={ref}
size="large"
>
<WbSunny />
</IconButton>
</SwitchThemeButton>
);
default:
return null;

View File

@ -7,16 +7,17 @@ import { useTranslation } from 'react-i18next';
import { Content, Title } from './styles';
import { Props } from './types';
const RegistryInfoDialog: React.FC<Props> = ({ open = false, children, onClose }) => {
const RegistryInfoDialog: React.FC<Props> = ({ open = false, children, onClose, title = '' }) => {
const { t } = useTranslation();
return (
<Dialog
data-testid={'registryInfo--dialog'}
id="registryInfo--dialog-container"
maxWidth="sm"
onClose={onClose}
open={open}
>
<Title>{t('dialog.registry-info.title')}</Title>
<Title>{title}</Title>
<Content>{children}</Content>
<DialogActions>
<Button color="inherit" id="registryInfo--dialog-close" onClick={onClose}>

View File

@ -3,5 +3,6 @@ import { ReactNode } from 'react';
export interface Props {
children: ReactNode;
open: boolean;
title: string;
onClose: () => void;
}

View File

@ -3,27 +3,11 @@
/* eslint-disable react/jsx-max-depth */
/* eslint-disable react/jsx-pascal-case */
import styled from '@emotion/styled';
import { Dialog, Link, Theme } from '@mui/material';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import { Link } from '@mui/material';
import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import FlagsIcon from 'country-flag-icons/react/3x2';
import React from 'react';
import flag from './uk.jpg';
const style = {
p: 4,
};
const Flags = styled('span')<{ theme?: Theme }>(() => ({
width: '25px',
}));
const title = 'Support people affected by the war in Ukraine';
const links = [
@ -54,10 +38,6 @@ const links = [
];
const Support = () => {
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const linkElements = links.map((link) => (
<li key={link.text}>
<Link href={link.href} target="_blank">
@ -68,53 +48,32 @@ const Support = () => {
return (
<>
<Tooltip title={title}>
<IconButton color="inherit" onClick={handleOpen} size="large">
<Flags>
<FlagsIcon.UA />
</Flags>
</IconButton>
</Tooltip>
<Dialog
aria-describedby="modal-modal-description"
aria-labelledby="modal-modal-title"
maxWidth="md"
onClose={handleClose}
open={open}
>
<Box sx={style}>
<Grid container={true} spacing={2}>
<Grid item={true} xs={12}>
<Typography component="h2" variant="h6">
{title}
</Typography>
<Divider />
</Grid>
<Grid item={true} lg={4} xs={12}>
<img alt={title} height="150" src={flag} />
</Grid>
<Grid item={true} lg={8} xs={12}>
<span style={{ fontStyle: 'italic', fontSize: '0.75rem' }}>
<Typography>
{`Hi, this is a message that I've composed to call your attention to ask
<Grid container={true} spacing={2}>
<Grid item={true} xs={12}>
<Typography component="h2" variant="h6">
{title}
</Typography>
</Grid>
<Grid item={true} lg={12} xs={12}>
<span style={{ fontStyle: 'italic', fontSize: '0.75rem' }}>
<Typography>
{`Hi, this is a message that I've composed to call your attention to ask
for humanitarian support for more than 44 million Ukrainians that are having
a hard time suffering for a horrible and unjustified war. It would be great if you
decide today to make a difference and help others. You could help by donating
to very well-known humanitarian organizations, helping in your local
area with food, clothes, donate blood, toys for kids, or your own time. Any help is very welcome.`}
</Typography>
</span>
<ul style={{ padding: '10px 0' }}>{linkElements}</ul>
<div>
<Typography variant="div">{`Spread the voice, make the difference today.`}</Typography>
</div>
<div style={{ padding: '10px 0', fontWeight: 600 }}>
<Typography variant="div">{`Att: Verdaccio Lead Mantainer, Juan P.`}</Typography>
</div>
</Grid>
</Grid>
</Box>
</Dialog>
</Typography>
</span>
<ul style={{ padding: '10px 0' }}>{linkElements}</ul>
<div>
<Typography variant="div">{`Spread the voice, make the difference today.`}</Typography>
</div>
<div style={{ padding: '10px 0', fontWeight: 600 }}>
<Typography variant="div">{`Att: Verdaccio Lead Mantainer, Juan P.`}</Typography>
</div>
</Grid>
</Grid>
</>
);
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,22 @@
Verdaccio is an open source lightweight private proxy Node.js registry,
you can [learn more about the project in our blog](https://verdaccio.org/blog/2019/02/08/the-crazy-story-of-verdaccio).
#### Useful links
- [Installation](https://verdaccio.org/docs/en/installation)
- [Translate](https://translate.verdaccio.org/)
- [Best practices](https://verdaccio.org/docs/best)
- [Security policy](https://github.com/verdaccio/verdaccio/security/policy)
- [Sponsor via open collective](https://opencollective.com/verdaccio)
- [Sponsor via GitHub](https://github.com/sponsors/verdaccio)
#### How to connect
- [Discussions](https://github.com/verdaccio/verdaccio/discussions)
- [YouTube channel](https://www.youtube.com/channel/UC5i20v6o7lSjXzAHOvatt0w)
- [Discord chat](https://discord.gg/hv42jfs)
- [Twitter community](https://twitter.com/i/communities/1502550839499579393)
https://www.verdaccio.org
#### Meet the contributors

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Verdaccio contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -42,9 +42,10 @@ export const InnerMobileNavBar = styled('div')<{ theme?: Theme }>((props) => ({
margin: '0 10px 0 0',
}));
export const IconSearchButton = styled(IconButton)({
display: 'block',
});
export const IconSearchButton = styled(IconButton)({});
export const InfoButton = styled(IconButton)({});
export const SwitchThemeButton = styled(IconButton)({});
export const SettingsButtom = styled(IconButton)({});
export const SearchWrapper = styled('div')({
display: 'none',
@ -58,13 +59,21 @@ export const NavBar = styled(AppBar)<{ theme?: Theme }>(({ theme }) => ({
minHeight: 60,
display: 'flex',
justifyContent: 'center',
[`@media (max-width: ${theme?.breakPoints.xsmall}px)`]: css`
${InfoButton} {
display: none;
}
${SwitchThemeButton} {
display: none;
}
${SettingsButtom} {
display: none;
}
`,
[`@media (min-width: ${theme?.breakPoints.medium}px)`]: css`
${SearchWrapper} {
display: flex;
}
${IconSearchButton} {
display: none;
}
${MobileNavBar} {
display: none;
}

View File

@ -1,9 +1,14 @@
import React from 'react';
import { cleanup, renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
import {
cleanup,
fireEvent,
renderWithStore,
screen,
} from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { store } from '../../store/store';
import ActionBar from './ActionBar';
import ActionBar, { Props } from './ActionBar';
const detailContextValue: DetailContextProps = {
packageName: 'foo',
@ -29,11 +34,12 @@ const detailContextValue: DetailContextProps = {
},
};
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps; props?: Props }> = ({
contextValue,
props,
}) => (
<DetailContext.Provider value={contextValue}>
<ActionBar />
<ActionBar {...props} />
</DetailContext.Provider>
);
@ -76,6 +82,42 @@ describe('<ActionBar /> component', () => {
expect(screen.getByLabelText('Download tarball')).toBeTruthy();
});
test('when there is a button to raw manifest', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: true }} />,
store
);
expect(screen.getByLabelText('Raw Manifest')).toBeTruthy();
});
test('when click button to raw manifest open a dialog with viewver', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: true }} />,
store
);
fireEvent.click(screen.getByLabelText('Raw Manifest'));
expect(screen.getByTestId('raw-viewver-dialog')).toBeInTheDocument();
});
test('should not display download tarball button', () => {
renderWithStore(
<ComponentToBeRendered
contextValue={{ ...detailContextValue }}
props={{ showDownloadTarball: false }}
/>,
store
);
expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
});
test('should not display show raw button', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: false }} />,
store
);
expect(screen.queryByLabelText('Raw Manifest')).toBeFalsy();
});
test('when there is a button to open an issue', () => {
renderWithStore(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />, store);
expect(screen.getByLabelText('Open an issue')).toBeTruthy();

View File

@ -1,12 +1,19 @@
import Box from '@mui/material/Box';
import React from 'react';
import React, { useState } from 'react';
import { isURL } from 'verdaccio-ui/utils/url';
import { DetailContext } from '../../pages/Version';
import RawViewer from '../RawViewer';
import ActionBarAction, { ActionBarActionProps } from './ActionBarAction';
export type Props = {
showRaw?: boolean;
showDownloadTarball?: boolean;
};
/* eslint-disable verdaccio/jsx-spread */
const ActionBar: React.FC = () => {
const ActionBar: React.FC<Props> = ({ showRaw, showDownloadTarball = true }) => {
const [isRawViewerOpen, setIsRawViewerOpen] = useState(false);
const detailContext = React.useContext(DetailContext);
const { packageMeta } = detailContext;
@ -27,15 +34,27 @@ const ActionBar: React.FC = () => {
actions.push({ type: 'OPEN_AN_ISSUE', link: bugs.url });
}
if (dist?.tarball && isURL(dist.tarball)) {
if (dist?.tarball && isURL(dist.tarball) && showDownloadTarball) {
actions.push({ type: 'DOWNLOAD_TARBALL', link: dist.tarball });
}
if (showRaw) {
actions.push({ type: 'RAW_DATA', action: () => setIsRawViewerOpen(true) });
}
return (
<Box alignItems="center" display="flex" marginBottom="14px">
{actions.map((action) => (
<ActionBarAction key={action.link} {...action} />
<ActionBarAction key={action.type} {...action} />
))}
{isRawViewerOpen && (
<RawViewer
isOpen={isRawViewerOpen}
onClose={() => {
setIsRawViewerOpen(false);
}}
/>
)}
</Box>
);
};

View File

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import BugReportIcon from '@mui/icons-material/BugReport';
import DownloadIcon from '@mui/icons-material/CloudDownload';
import HomeIcon from '@mui/icons-material/Home';
import RawOnIcon from '@mui/icons-material/RawOn';
import Tooltip from '@mui/material/Tooltip';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -23,15 +24,16 @@ export const Fab = styled(FloatingActionButton)<{ theme?: Theme }>(({ theme }) =
},
}));
type ActionType = 'VISIT_HOMEPAGE' | 'OPEN_AN_ISSUE' | 'DOWNLOAD_TARBALL';
type ActionType = 'VISIT_HOMEPAGE' | 'OPEN_AN_ISSUE' | 'DOWNLOAD_TARBALL' | 'RAW_DATA';
export interface ActionBarActionProps {
type: ActionType;
link: string;
link?: string;
action?: () => void;
}
/* eslint-disable react/jsx-no-bind */
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link, action }) => {
const { t } = useTranslation();
const dispatch = useDispatch<Dispatch>();
@ -42,7 +44,7 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
switch (type) {
case 'VISIT_HOMEPAGE':
return (
<Tooltip title={t('action-bar-action.visit-home-page')}>
<Tooltip title={t('action-bar-action.visit-home-page') as string}>
<Link external={true} to={link}>
<Fab size="small">
<HomeIcon />
@ -52,7 +54,7 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
);
case 'OPEN_AN_ISSUE':
return (
<Tooltip title={t('action-bar-action.open-an-issue')}>
<Tooltip title={t('action-bar-action.open-an-issue') as string}>
<Link external={true} to={link}>
<Fab size="small">
<BugReportIcon />
@ -62,12 +64,20 @@ const ActionBarAction: React.FC<ActionBarActionProps> = ({ type, link }) => {
);
case 'DOWNLOAD_TARBALL':
return (
<Tooltip title={t('action-bar-action.download-tarball')}>
<Tooltip title={t('action-bar-action.download-tarball') as string}>
<Fab data-testid="download-tarball-btn" onClick={handleDownload} size="small">
<DownloadIcon />
</Fab>
</Tooltip>
);
case 'RAW_DATA':
return (
<Tooltip title={t('action-bar-action.raw') as string}>
<Fab data-testid="raw-btn" onClick={action} size="small">
<RawOnIcon />
</Fab>
</Tooltip>
);
}
};

View File

@ -3,8 +3,8 @@ import React from 'react';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
import blackAndWithLogo from './img/logo-uk.svg';
import defaultLogo from './img/logo-uk.svg';
import blackAndWithLogo from './img/logo-black-and-white.svg';
import defaultLogo from './img/logo.svg';
const sizes = {
'x-small': '30px',

View File

@ -1,38 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="37" fill="#F7F8F6"/>
<g filter="url(#filter0_d_0_18)">
<mask id="path-2-inside-1_0_18" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70 46.6L54.8 77H46L22.4 29.8L37.6 29.8L50.4 55.4L54.8 46.6H70Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70 46.6L54.8 77H46L22.4 29.8L37.6 29.8L50.4 55.4L54.8 46.6H70Z" fill="#005EB8"/>
<path d="M70 46.6L72.1466 47.6733L73.8833 44.2H70V46.6ZM54.8 77V79.4H56.2833L56.9466 78.0733L54.8 77ZM46 77L43.8534 78.0733L44.5167 79.4H46V77ZM22.4 29.8V27.4H18.5167L20.2534 30.8733L22.4 29.8ZM37.6 29.8L39.7466 28.7267L39.0833 27.4H37.6V29.8ZM50.4 55.4L48.2534 56.4733L50.4 60.7666L52.5466 56.4733L50.4 55.4ZM54.8 46.6V44.2H53.3167L52.6534 45.5267L54.8 46.6ZM67.8534 45.5267L52.6534 75.9267L56.9466 78.0733L72.1466 47.6733L67.8534 45.5267ZM54.8 74.6H46V79.4H54.8V74.6ZM48.1466 75.9267L24.5466 28.7267L20.2534 30.8733L43.8534 78.0733L48.1466 75.9267ZM22.4 32.2L37.6 32.2V27.4L22.4 27.4V32.2ZM35.4534 30.8733L48.2534 56.4733L52.5466 54.3267L39.7466 28.7267L35.4534 30.8733ZM52.5466 56.4733L56.9466 47.6733L52.6534 45.5267L48.2534 54.3267L52.5466 56.4733ZM54.8 49H70V44.2H54.8V49Z" fill="#005EB8" mask="url(#path-2-inside-1_0_18)"/>
</g>
<g filter="url(#filter1_d_0_18)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.8 41H57.6L63.2 29.8L78.4 29.8L72.8 41Z" fill="#005EB8"/>
<path d="M76.4584 31L72.0584 39.8H59.5416L63.9416 31H76.4584Z" stroke="#005EB8" stroke-width="2.4"/>
</g>
<path d="M56.6351 70.688L54.0607 75.8H46.7416L24.3416 31H36.8573L56.6351 70.688Z" fill="#FFD101" stroke="#FFD101" stroke-width="2.4"/>
<path d="M59.6 31H74.821" stroke="#005EB8" stroke-width="2.4" stroke-linecap="square"/>
<path d="M55.6 35H70.821" stroke="#005EB8" stroke-width="2.4" stroke-linecap="square"/>
<path d="M51.6 39.8H66.821" stroke="#005EB8" stroke-width="2.4" stroke-linecap="square"/>
<defs>
<filter id="filter0_d_0_18" x="17.4" y="28.8" width="57.6" height="57.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0906646 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_18"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_18" result="shape"/>
</filter>
<filter id="filter1_d_0_18" x="52.6" y="28.8" width="30.8" height="21.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0906646 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_18"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_18" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,76 @@
import CloseIcon from '@mui/icons-material/Close';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactJson from 'react-json-view';
import { DetailContext } from '../../pages/Version';
export interface ViewerTitleProps {
id: string;
children?: React.ReactNode;
onClose: () => void;
}
const ViewerTitle = (props: ViewerTitleProps) => {
const { children, onClose, ...other } = props;
return (
<DialogTitle sx={{ m: 0, p: 2 }} {...other} data-testid="raw-viewver-dialog">
{children}
{onClose ? (
<IconButton
aria-label="close"
onClick={onClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: (theme) => theme.palette.grey[500],
}}
>
<CloseIcon />
</IconButton>
) : null}
</DialogTitle>
);
};
type Props = {
isOpen: boolean;
onClose: () => void;
};
/* eslint-disable verdaccio/jsx-spread */
const RawViewer: React.FC<Props> = ({ isOpen = false, onClose }) => {
const detailContext = React.useContext(DetailContext);
const { t } = useTranslation();
const { packageMeta } = detailContext;
return (
<Dialog
data-testid={'rawViewer--dialog'}
id="raw-viewer--dialog-container"
fullScreen={true}
open={isOpen}
>
<ViewerTitle id="viewer-title" onClose={onClose}>
{t('action-bar-action.raw')}
</ViewerTitle>
<DialogContent>
<ReactJson
src={packageMeta as any}
collapsed={true}
collapseStringsAfterLength={40}
groupArraysAfterLength={10}
enableClipboard={false}
/>
</DialogContent>
</Dialog>
);
};
export default RawViewer;

View File

@ -0,0 +1 @@
export { default } from './RawViewer';

View File

@ -13,15 +13,23 @@ declare module '@mui/styles/defaultTheme' {
interface DefaultTheme extends Theme {}
}
const ThemeProviderWrapper: React.FC = ({ children }) => {
function getDarkModeDefault(darkModeConfig) {
const prefersDarkMode = window.matchMedia?.('(prefers-color-scheme:dark)').matches;
const isDarkModeDefault = window?.__VERDACCIO_BASENAME_UI_OPTIONS?.darkMode || prefersDarkMode;
if (typeof darkModeConfig === 'boolean') {
return darkModeConfig;
} else {
return prefersDarkMode;
}
}
const ThemeProviderWrapper: React.FC = ({ children }) => {
const currentLanguage = i18next.languages?.[0];
const { configOptions } = useConfig();
const [isDarkMode, setIsDarkMode] = useLocalStorage('darkMode', !!isDarkModeDefault);
const isDarkModeDefault = getDarkModeDefault(configOptions.darkMode);
const isSwitchThemeEnabled = configOptions.showThemeSwitch;
const [isDarkModeStorage, setIsDarkMode] = useLocalStorage('darkMode', isDarkModeDefault);
const [language, setLanguage] = useLocalStorage('language', currentLanguage);
const isDarkMode = isSwitchThemeEnabled === true ? isDarkModeStorage : isDarkModeDefault;
const themeMode: ThemeMode = isDarkMode ? 'dark' : 'light';
const changeLanguage = useCallback(async () => {

View File

@ -73,6 +73,7 @@ const fontWeight = {
export type FontWeight = keyof typeof fontWeight;
export const breakPoints = {
xsmall: 400,
small: 576,
medium: 768,
large: 1024,

View File

@ -5,17 +5,25 @@
"action-bar-action": {
"visit-home-page": "Visit homepage",
"open-an-issue": "Open an issue",
"download-tarball": "Download tarball"
"download-tarball": "Download tarball",
"raw": "Raw Manifest"
},
"dialog": {
"registry-info": {
"title": "Configuration Details"
}
"title": "Information"
},
"settings": {
"title": "Configuration"
},
"license": "License",
"totalContributors": "Total contributors"
},
"header": {
"documentation": "Documentation",
"registry-info": "Registry Information",
"registry-info-link": "Learn more",
"settings": "Settings",
"help": "Help",
"registry-no-conf": "No configurations available",
"greetings": "Hi "
},
@ -152,13 +160,14 @@
"portuguese": "Portuguese",
"spanish": "Spanish",
"german": "German",
"chinese": "Chinese",
"chineseTraditional": "Chinese (Traditional)",
"italian": "Italian",
"french": "French",
"russian": "Russian",
"turkish": "Turkish",
"ukraine": "Ukraine",
"khmer": "Khmer"
"khmer": "Khmer",
"chinese": "Chinese Simplified",
"chineseTraditional": "Chinese Traditional"
},
"flag": {
"austria": "Austria",

View File

@ -15,13 +15,14 @@ export const listLanguages: LanguageConfiguration[] = [
{ lng: 'pt-BR', icon: Flags.BR, menuKey: 'lng.portuguese' },
{ lng: 'es-ES', icon: Flags.ES, menuKey: 'lng.spanish' },
{ lng: 'de-DE', icon: Flags.DE, menuKey: 'lng.german' },
{ lng: 'it-IT', icon: Flags.IT, menuKey: 'lng.italian' },
{ lng: 'fr-FR', icon: Flags.FR, menuKey: 'lng.french' },
{ lng: 'zh-CN', icon: Flags.CN, menuKey: 'lng.chinese' },
{ lng: 'ja-JP', icon: Flags.JP, menuKey: 'lng.japanese' },
{ lng: 'ru-RU', icon: Flags.RU, menuKey: 'lng.russian' },
{ lng: 'tr-TR', icon: Flags.TR, menuKey: 'lng.turkish' },
{ lng: 'uk-UA', icon: Flags.UA, menuKey: 'lng.ukraine' },
{ lng: 'km-KH', icon: Flags.KH, menuKey: 'lng.khme' },
{ lng: 'zh-CN', icon: Flags.CN, menuKey: 'lng.chinese' },
{ lng: 'zh-TW', icon: Flags.TW, menuKey: 'lng.chineseTraditional' },
];

View File

@ -5,6 +5,7 @@ import ActionBar from 'verdaccio-ui/components/ActionBar';
import Author from 'verdaccio-ui/components/Author';
import Paper from 'verdaccio-ui/components/Paper';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
import { DetailContext } from '..';
import loadable from '../../../App/utils/loadable';
@ -30,6 +31,7 @@ const getModuleType = (manifest: PackageMetaInterface) => {
const DetailSidebar: React.FC = () => {
const detailContext = useContext(DetailContext);
const { packageMeta, packageName, packageVersion } = detailContext;
const { configOptions } = useConfig();
if (!packageMeta || !packageName) {
return null;
@ -45,7 +47,10 @@ const DetailSidebar: React.FC = () => {
packageName={packageName}
version={packageVersion || packageMeta.latest.version}
/>
<ActionBar />
<ActionBar
showDownloadTarball={configOptions.showDownloadTarball}
showRaw={configOptions.showRaw}
/>
<Install />
<DetailSidebarFundButton />
<Repository />

View File

@ -24,7 +24,6 @@ const Install: React.FC = () => {
if (!packageMeta || !packageName) {
return null;
}
const hasNpm = configOptions?.pkgManagers?.includes('npm');
const hasYarn = configOptions?.pkgManagers?.includes('yarn');
const hasPnpm = configOptions?.pkgManagers?.includes('pnpm') ?? true;

View File

@ -55,6 +55,7 @@ export interface PackageInterface {
homepage?: string;
bugs?: Bugs;
dist?: Dist;
showDownload?: boolean;
}
const Package: React.FC<PackageInterface> = ({
@ -67,6 +68,7 @@ const Package: React.FC<PackageInterface> = ({
license,
name: packageName,
time,
showDownload = true,
version,
}) => {
const config = useSelector((state: RootState) => state.configuration.config);
@ -189,7 +191,7 @@ const Package: React.FC<PackageInterface> = ({
>
{renderHomePageLink()}
{renderBugsLink()}
{renderDownloadLink()}
{showDownload && renderDownloadLink()}
</GridRightAligned>
</Grid>
);

View File

@ -4,6 +4,7 @@ import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import { CellMeasurer, CellMeasurerCache } from 'react-virtualized/dist/commonjs/CellMeasurer';
import { List, ListRowProps } from 'react-virtualized/dist/commonjs/List';
import { WindowScroller } from 'react-virtualized/dist/commonjs/WindowScroller';
import { useConfig } from 'verdaccio-ui/providers/config';
import { formatLicense } from 'verdaccio-ui/utils/package';
import Help from './Help';
@ -20,6 +21,7 @@ const cache = new CellMeasurerCache({
/* eslint-disable verdaccio/jsx-no-style */
const PackageList: React.FC<Props> = ({ packages }) => {
const { configOptions } = useConfig();
const renderRow = ({ index, key, parent, style }: ListRowProps) => {
const { name, version, description, time, keywords, dist, homepage, bugs, author, license } =
packages[index];
@ -38,6 +40,7 @@ const PackageList: React.FC<Props> = ({ packages }) => {
keywords={keywords}
license={formattedLicense}
name={name}
showDownload={configOptions.showDownloadTarball}
time={time}
version={version}
/>

View File

@ -1,5 +1,6 @@
import isEmpty from 'lodash/isEmpty';
import isNil from 'lodash/isNil';
import merge from 'lodash/merge';
import React, { FunctionComponent, createContext, useContext, useMemo, useState } from 'react';
import { PRIMARY_COLOR } from 'verdaccio-ui/utils/colors';
@ -12,14 +13,21 @@ type ConfigProviderProps = {
const defaultValues: ConfigProviderProps = {
configOptions: {
// note: dark mode set as undefined by design
primaryColor: PRIMARY_COLOR,
darkMode: false,
pkgManagers: ['yarn', 'pnpm', 'npm'],
scope: '',
base: '',
flags: {},
login: true,
url_prefix: '',
showInfo: true,
showSettings: true,
showThemeSwitch: true,
showFooter: true,
showSearch: true,
showRaw: true,
showDownloadTarball: true,
title: 'Verdaccio',
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -27,7 +35,15 @@ const defaultValues: ConfigProviderProps = {
};
function getConfiguration() {
const uiConfiguration = window?.__VERDACCIO_BASENAME_UI_OPTIONS ?? defaultValues.configOptions;
const uiConfiguration = merge(
defaultValues.configOptions,
window?.__VERDACCIO_BASENAME_UI_OPTIONS
);
if (window?.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers) {
uiConfiguration.pkgManagers = window?.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers;
}
if (isNil(uiConfiguration.primaryColor) || isEmpty(uiConfiguration.primaryColor)) {
uiConfiguration.primaryColor = PRIMARY_COLOR;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -2,7 +2,8 @@ web:
title: Verdaccio Local Dev
sort_packages: asc
primary_color: #CCC
login: true
# showRaw: true
# darkMode: true
pkgManagers:
- npm
- yarn

View File

@ -67,6 +67,10 @@ module.exports = {
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.md$/,
use: 'raw-loader',
},
/* Typescript loader */
{
test: /\.tsx?$/,

View File

@ -7,3 +7,13 @@ declare module '*.png' {
const value: string;
export = value;
}
declare module '*.jpg' {
const value: string;
export = value;
}
declare module '*.md' {
const value: string;
export = value;
}

View File

@ -780,11 +780,13 @@ importers:
normalize.css: 8.0.1
optimize-css-assets-webpack-plugin: 6.0.1
ora: 5.4.1
raw-loader: 4.0.2
react: 17.0.2
react-dom: 17.0.2
react-hook-form: 7.25.0
react-hot-loader: 4.13.0
react-i18next: 11.15.3
react-json-view: 1.21.3
react-markdown: 8.0.0
react-redux: 7.2.6
react-router: 5.2.1
@ -859,11 +861,13 @@ importers:
normalize.css: 8.0.1
optimize-css-assets-webpack-plugin: 6.0.1_webpack@5.67.0
ora: 5.4.1
raw-loader: 4.0.2_webpack@5.67.0
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-hook-form: 7.25.0_react@17.0.2
react-hot-loader: 4.13.0_b3482aaf5744fc7c2aeb7941b0e0a78f
react-i18next: 11.15.3_ad209b3ec0793904285d43906e66750b
react-json-view: 1.21.3_b3482aaf5744fc7c2aeb7941b0e0a78f
react-markdown: 8.0.0_b08e3c15324cbe90a6ff8fcd416c932c
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
react-router: 5.2.1_react@17.0.2
@ -11139,7 +11143,6 @@ packages:
/base16/1.0.0:
resolution: {integrity: sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=}
dev: false
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -12382,7 +12385,6 @@ packages:
node-fetch: 2.6.7
transitivePeerDependencies:
- encoding
dev: false
/cross-spawn/5.1.0:
resolution: {integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=}
@ -14899,11 +14901,9 @@ packages:
fbjs: 3.0.4
transitivePeerDependencies:
- encoding
dev: false
/fbjs-css-vars/1.0.2:
resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
dev: false
/fbjs/3.0.4:
resolution: {integrity: sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==}
@ -14917,7 +14917,6 @@ packages:
ua-parser-js: 0.7.31
transitivePeerDependencies:
- encoding
dev: false
/fd-slicer/1.1.0:
resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=}
@ -15092,7 +15091,6 @@ packages:
react: 17.0.2
transitivePeerDependencies:
- encoding
dev: false
/follow-redirects/1.13.0:
resolution: {integrity: sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==}
@ -18223,7 +18221,6 @@ packages:
big.js: 5.2.2
emojis-list: 3.0.0
json5: 2.2.0
dev: false
/loader-utils/3.2.0:
resolution: {integrity: sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==}
@ -18295,7 +18292,6 @@ packages:
/lodash.curry/4.1.1:
resolution: {integrity: sha1-JI42By7ekGUB11lmIAqG2riyMXA=}
dev: false
/lodash.debounce/4.0.8:
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
@ -18313,7 +18309,6 @@ packages:
/lodash.flow/3.5.0:
resolution: {integrity: sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=}
dev: false
/lodash.foreach/4.5.0:
resolution: {integrity: sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=}
@ -21808,7 +21803,6 @@ packages:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
dependencies:
asap: 2.0.6
dev: false
/prompts/2.4.0:
resolution: {integrity: sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==}
@ -21970,7 +21964,6 @@ packages:
/pure-color/1.3.0:
resolution: {integrity: sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=}
dev: false
/q/1.5.1:
resolution: {integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=}
@ -22075,6 +22068,17 @@ packages:
unpipe: 1.0.0
dev: false
/raw-loader/4.0.2_webpack@5.67.0:
resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==}
engines: {node: '>= 10.13.0'}
peerDependencies:
webpack: ^4.0.0 || ^5.0.0
dependencies:
loader-utils: 2.0.2
schema-utils: 3.1.1
webpack: 5.67.0_webpack-cli@4.7.2
dev: true
/rc/1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@ -22091,7 +22095,6 @@ packages:
lodash.curry: 4.1.1
lodash.flow: 3.5.0
pure-color: 1.3.0
dev: false
/react-dev-utils/11.0.4:
resolution: {integrity: sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A==}
@ -22267,6 +22270,23 @@ packages:
/react-is/17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
/react-json-view/1.21.3_b3482aaf5744fc7c2aeb7941b0e0a78f:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
dependencies:
flux: 4.0.3_react@17.0.2
react: 17.0.2
react-base16-styling: 0.6.0
react-dom: 17.0.2_react@17.0.2
react-lifecycles-compat: 3.0.4
react-textarea-autosize: 8.3.3_b08e3c15324cbe90a6ff8fcd416c932c
transitivePeerDependencies:
- '@types/react'
- encoding
dev: true
/react-json-view/1.21.3_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies:
@ -22462,6 +22482,20 @@ packages:
react: 17.0.2
dev: false
/react-textarea-autosize/8.3.3_b08e3c15324cbe90a6ff8fcd416c932c:
resolution: {integrity: sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0
dependencies:
'@babel/runtime': 7.17.2
react: 17.0.2
use-composed-ref: 1.2.1_react@17.0.2
use-latest: 1.2.0_b08e3c15324cbe90a6ff8fcd416c932c
transitivePeerDependencies:
- '@types/react'
dev: true
/react-textarea-autosize/8.3.3_react@17.0.2:
resolution: {integrity: sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==}
engines: {node: '>=10'}
@ -23460,7 +23494,6 @@ packages:
/setimmediate/1.0.5:
resolution: {integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=}
dev: false
/setprototypeof/1.1.0:
resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==}
@ -25107,7 +25140,6 @@ packages:
/ua-parser-js/0.7.31:
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
dev: false
/uglify-js/3.12.4:
resolution: {integrity: sha512-L5i5jg/SHkEqzN18gQMTWsZk3KelRsfD1wUVNqtq0kzqWQqcJjyL8yc1o8hJgRrWqrAl2mUFbhfznEIoi7zi2A==}
@ -25492,7 +25524,6 @@ packages:
react: ^16.8.0 || ^17.0.0
dependencies:
react: 17.0.2
dev: false
/use-is-in-viewport/1.0.9_react@17.0.2:
resolution: {integrity: sha512-Dgi0z/X9eTk3ziI+b28mZVoYtCtyoUFQ+9VBq6fR5EdjqmmsSlbr8ysXAwmEl89OUNBQwVGLGdI9nqwiu3168g==}
@ -25504,6 +25535,19 @@ packages:
react: 17.0.2
dev: false
/use-isomorphic-layout-effect/1.1.1_b08e3c15324cbe90a6ff8fcd416c932c:
resolution: {integrity: sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 17.0.38
react: 17.0.2
dev: true
/use-isomorphic-layout-effect/1.1.1_react@17.0.2:
resolution: {integrity: sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==}
peerDependencies:
@ -25516,6 +25560,20 @@ packages:
react: 17.0.2
dev: false
/use-latest/1.2.0_b08e3c15324cbe90a6ff8fcd416c932c:
resolution: {integrity: sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 17.0.38
react: 17.0.2
use-isomorphic-layout-effect: 1.1.1_b08e3c15324cbe90a6ff8fcd416c932c
dev: true
/use-latest/1.2.0_react@17.0.2:
resolution: {integrity: sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==}
peerDependencies:

View File

@ -5,6 +5,7 @@ import path from 'path';
const token = process.env.TOKEN;
const excludebots = [
'verdacciobot',
'github-actions[bot]',
'dependabot-preview[bot]',
'dependabot[bot]',
'64b2b6d12bfe4baae7dad3d01',
@ -30,7 +31,19 @@ const excludebots = [
__dirname,
'../packages/tools/docusaurus-plugin-contributors/src/contributors.json'
);
// for the website
await fs.writeFile(pathContributorsFile, JSON.stringify(result, null, 4));
const contributorsListId = result.map((contributor: any) => {
return { username: contributor?.login, id: contributor.id };
});
// .sort()
// .slice(0, 15);
// for the ui, list of ids to be added on the contributors.
const pathContributorsUIFile = path.join(
__dirname,
'../packages/plugins/ui-theme/src/App/Header/generated_contributors_list.json'
);
await fs.writeFile(pathContributorsUIFile, JSON.stringify(contributorsListId, null, 4));
} catch (err) {
// eslint-disable-next-line no-console
console.error('error on update', err);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB