mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-24 21:15:51 +01:00
feat: improve language switch ui and package manager info (#2936)
* feat: improve language switch ui and package manager info * feat: improve registry info dialog and language switch * add description * update text * update npmignore * chore: update test expect
This commit is contained in:
parent
7bb3c2bf0e
commit
7ff4808be6
5
.changeset/tiny-seals-join.md
Normal file
5
.changeset/tiny-seals-join.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'@verdaccio/ui-theme': minor
|
||||
---
|
||||
|
||||
feat: improve registry info dialog and language switch
|
@ -112,7 +112,7 @@
|
||||
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
|
||||
"lint": "eslint --max-warnings 47 \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint": "eslint --max-warnings 44 \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"test": "pnpm recursive test --filter ./packages",
|
||||
"test:e2e:cli": "pnpm test --filter ...@verdaccio/e2e-cli",
|
||||
"test:e2e:ui": "pnpm test --filter ...@verdaccio/e2e-ui",
|
||||
|
4
packages/plugins/ui-theme/.npmignore
Normal file
4
packages/plugins/ui-theme/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
/*
|
||||
!/static/**/*
|
||||
!index.js
|
||||
!README.md
|
@ -35,5 +35,7 @@ module.exports = Object.assign({}, config, {
|
||||
'verdaccio-ui/utils/(.*)': '<rootDir>/src/utils/$1',
|
||||
'verdaccio-ui/providers/(.*)': '<rootDir>/src/providers/$1',
|
||||
'verdaccio-ui/design-tokens/(.*)': '<rootDir>/src/design-tokens/$1',
|
||||
'react-markdown': '<rootDir>/src/__mocks__/react-markdown.tsx',
|
||||
'remark-*': '<rootDir>/src/__mocks__/remark-plugin.ts',
|
||||
},
|
||||
});
|
||||
|
@ -60,6 +60,8 @@
|
||||
"mutationobserver-shim": "0.3.7",
|
||||
"node-mocks-http": "1.11.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"react-markdown": "8.0.0",
|
||||
"remark-gfm": "3.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "6.0.1",
|
||||
"ora": "5.4.1",
|
||||
"react": "17.0.2",
|
||||
|
@ -23,7 +23,6 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
||||
const [showMobileNavBar, setShowMobileNavBar] = useState<boolean>(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState<boolean>(false);
|
||||
const loginStore = useSelector((state: RootState) => state.login);
|
||||
const configStore = useSelector((state: RootState) => state.configuration.config);
|
||||
const { configOptions } = useConfig();
|
||||
const dispatch = useDispatch<Dispatch>();
|
||||
const handleLogout = () => {
|
||||
@ -45,12 +44,12 @@ const Header: React.FC<Props> = ({ withoutSearch }) => {
|
||||
withoutSearch={withoutSearch}
|
||||
/>
|
||||
</InnerNavBar>
|
||||
<HeaderInfoDialog
|
||||
isOpen={isInfoDialogOpen}
|
||||
onCloseDialog={() => setOpenInfoDialog(false)}
|
||||
registryUrl={configOptions.base}
|
||||
scope={configStore.scope}
|
||||
/>
|
||||
{
|
||||
<HeaderInfoDialog
|
||||
isOpen={isInfoDialogOpen}
|
||||
onCloseDialog={() => setOpenInfoDialog(false)}
|
||||
/>
|
||||
}
|
||||
</NavBar>
|
||||
{showMobileNavBar && !withoutSearch && (
|
||||
<MobileNavBar>
|
||||
|
@ -1,19 +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;
|
||||
registryUrl: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
const HeaderInfoDialog: React.FC<Props> = ({ onCloseDialog, isOpen, registryUrl, scope }) => (
|
||||
<RegistryInfoDialog onClose={onCloseDialog} open={isOpen}>
|
||||
<RegistryInfoContent registryUrl={registryUrl} scope={scope} />
|
||||
</RegistryInfoDialog>
|
||||
);
|
||||
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 HeaderInfoDialog: 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}>
|
||||
<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 HeaderInfoDialog;
|
||||
|
@ -52,12 +52,7 @@ const HeaderMenu: React.FC<Props> = ({
|
||||
<MenuItem>
|
||||
<HeaderGreetings username={username} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
button={true}
|
||||
data-testid="logOutDialogIcon"
|
||||
id="logOutDialogIcon"
|
||||
onClick={onLogout}
|
||||
>
|
||||
<MenuItem data-testid="logOutDialogIcon" id="logOutDialogIcon" onClick={onLogout}>
|
||||
{t('button.logout')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
@ -5,7 +5,6 @@ import ThemeContext from 'verdaccio-ui/design-tokens/ThemeContext';
|
||||
|
||||
import HeaderMenu from './HeaderMenu';
|
||||
import HeaderToolTip from './HeaderToolTip';
|
||||
import LanguageSwitch from './LanguageSwitch';
|
||||
import { RightSide } from './styles';
|
||||
|
||||
interface Props {
|
||||
@ -79,7 +78,6 @@ const HeaderRight: React.FC<Props> = ({
|
||||
tooltipIconType={'search'}
|
||||
/>
|
||||
)}
|
||||
<LanguageSwitch />
|
||||
<HeaderToolTip title={t('header.documentation')} tooltipIconType={'help'} />
|
||||
<HeaderToolTip
|
||||
onClick={onOpenRegistryInfoDialog}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'verdaccio-ui/components/Tooltip';
|
||||
|
||||
import HeaderToolTipIcon, { TooltipIconType } from './HeaderToolTipIcon';
|
||||
|
||||
@ -9,10 +8,8 @@ interface Props {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const HeaderToolTip: React.FC<Props> = ({ tooltipIconType, title, onClick }) => (
|
||||
<Tooltip disableFocusListener={true} title={title}>
|
||||
<HeaderToolTipIcon onClick={onClick} tooltipIconType={tooltipIconType} />
|
||||
</Tooltip>
|
||||
const HeaderToolTip: React.FC<Props> = ({ tooltipIconType, onClick }) => (
|
||||
<HeaderToolTipIcon onClick={onClick} tooltipIconType={tooltipIconType} />
|
||||
);
|
||||
|
||||
export default HeaderToolTip;
|
||||
|
@ -50,7 +50,7 @@ const HeaderToolTipIcon = forwardRef<HeaderToolTipIconRef, Props>(function Heade
|
||||
);
|
||||
case 'search':
|
||||
return (
|
||||
<IconSearchButton color="inherit" onClick={onClick}>
|
||||
<IconSearchButton color="inherit" onClick={onClick} ref={ref}>
|
||||
<Search />
|
||||
</IconSearchButton>
|
||||
);
|
||||
|
@ -1,12 +1,12 @@
|
||||
/* eslint-disable react/jsx-pascal-case */
|
||||
import styled from '@emotion/styled';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import withStyles from '@mui/styles/withStyles';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AutoComplete } from 'verdaccio-ui/components/AutoComplete/AutoCompleteV2';
|
||||
import MenuItem from 'verdaccio-ui/components/MenuItem';
|
||||
import ThemeContext from 'verdaccio-ui/design-tokens/ThemeContext';
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
|
||||
@ -20,26 +20,42 @@ const listConverted = listLanguages.reduce((prev, item) => {
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
export const CardSelected = styled(Card)<{ theme?: Theme }>(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme?.palette?.grey['600'],
|
||||
opacity: '0.9',
|
||||
color: theme?.palette?.error.contrastText,
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
});
|
||||
|
||||
export const CardUnSelected = styled(Card)<{ theme?: Theme }>(({ theme }) => {
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: theme?.palette.greyGainsboro,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const LanguageContent = ({ translation, icon }) => (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography display="block" gutterBottom={true} variant="caption">
|
||||
{translation}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardContent>{icon}</CardContent>
|
||||
</>
|
||||
);
|
||||
|
||||
const LanguageSwitch = () => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const themeContext = useContext(ThemeContext) as any;
|
||||
const [languages] = useState<Language[]>(
|
||||
Object.keys(i18next.options?.resources || {}) as Language[]
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!themeContext) {
|
||||
throw Error(t('theme-context-not-correct-used'));
|
||||
}
|
||||
|
||||
const currentLanguage = themeContext.language;
|
||||
|
||||
const switchLanguage = useCallback(
|
||||
({ language }: { language: Language }) => {
|
||||
themeContext.setLanguage(language);
|
||||
},
|
||||
[themeContext]
|
||||
);
|
||||
|
||||
const getCurrentLngDetails = useCallback(
|
||||
(language: Language) => {
|
||||
const lng = listConverted[language] || listConverted['en-US'];
|
||||
@ -50,75 +66,35 @@ const LanguageSwitch = () => {
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
languages.map((language) => {
|
||||
const { icon, translation } = getCurrentLngDetails(language);
|
||||
return {
|
||||
language,
|
||||
icon,
|
||||
translation,
|
||||
};
|
||||
}),
|
||||
[languages, getCurrentLngDetails]
|
||||
);
|
||||
|
||||
const option = useCallback(
|
||||
({ icon, translation }: ReturnType<typeof getCurrentLngDetails>) => (
|
||||
<StyledMenuItem component="div">
|
||||
{icon}
|
||||
{translation}
|
||||
</StyledMenuItem>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const optionLabel = useCallback(
|
||||
({ translation }: ReturnType<typeof getCurrentLngDetails>) => translation,
|
||||
[]
|
||||
const handleChangeLanguage = useCallback(
|
||||
(language: Language) => {
|
||||
themeContext.setLanguage(language);
|
||||
},
|
||||
[themeContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<AutoComplete
|
||||
defaultValue={getCurrentLngDetails(currentLanguage).translation}
|
||||
getOptionLabel={optionLabel}
|
||||
hasClearIcon={true}
|
||||
inputStartAdornment={<StyledLanguageIcon />}
|
||||
onClick={switchLanguage}
|
||||
options={options}
|
||||
renderOption={option}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Wrapper>
|
||||
<div>
|
||||
<Grid columns={{ xs: 12, sm: 12, md: 12 }} container={true} spacing={{ xs: 1, md: 1 }}>
|
||||
{languages.map((language, index) => {
|
||||
const { icon, translation } = getCurrentLngDetails(language);
|
||||
return (
|
||||
<Grid item={true} key={index} sm={2} xs={6}>
|
||||
{language === currentLanguage ? (
|
||||
<CardSelected sx={{ maxWidth: 80 }}>
|
||||
<LanguageContent icon={icon} translation={translation} />
|
||||
</CardSelected>
|
||||
) : (
|
||||
<CardUnSelected onClick={() => handleChangeLanguage(language)}>
|
||||
<LanguageContent icon={icon} translation={translation} />
|
||||
</CardUnSelected>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitch;
|
||||
|
||||
const StyledLanguageIcon = styled(LanguageIcon)<{ theme?: Theme }>(({ theme }) => ({
|
||||
color: theme?.palette.white,
|
||||
}));
|
||||
|
||||
const Wrapper = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
||||
width: 220,
|
||||
display: 'none',
|
||||
[`@media screen and (min-width: ${theme?.breakPoints.medium}px)`]: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledMenuItem = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
display: 'grid',
|
||||
cursor: 'pointer',
|
||||
gridGap: theme?.spacing(0.5),
|
||||
gridTemplateColumns: '20px 1fr',
|
||||
alignItems: 'center',
|
||||
'&:first-child': {
|
||||
borderTopLeftRadius: 4,
|
||||
borderTopRightRadius: 4,
|
||||
},
|
||||
},
|
||||
}))(MenuItem);
|
||||
|
@ -9,8 +9,10 @@ describe('<RegistryInfoContent /> component', () => {
|
||||
});
|
||||
|
||||
test('should load the component with no data', () => {
|
||||
render(<RegistryInfoContent registryUrl={''} scope={''} />);
|
||||
expect(screen.getByText('No configurations available')).toBeInTheDocument();
|
||||
render(<RegistryInfoContent registryUrl="http://localhost:4873" scope={''} />);
|
||||
expect(
|
||||
screen.getByText(/This is the configuration details for the registry./)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should load the appropiate tab content when the tab is clicked', () => {
|
||||
|
@ -9,18 +9,23 @@ import Typography from '@mui/material/Typography';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import CopyToClipBoard from 'verdaccio-ui/components/CopyToClipBoard';
|
||||
import { useConfig } from 'verdaccio-ui/providers/config';
|
||||
import {
|
||||
getCLIChangePassword,
|
||||
getCLISBerryYamlRegistry,
|
||||
getCLISetConfigRegistry,
|
||||
getCLISetRegistry,
|
||||
} from 'verdaccio-ui/utils/cli-utils';
|
||||
import { NODE_MANAGER } from 'verdaccio-ui/utils/constants';
|
||||
|
||||
const renderNpmTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
import { Description, TextContent } from './styles';
|
||||
|
||||
const renderNpmTab = (scope: string | undefined, registryUrl: string): JSX.Element => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" p={1}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<CopyToClipBoard
|
||||
text={getCLISetConfigRegistry(`${NODE_MANAGER.npm} set`, scope, registryUrl)}
|
||||
/>
|
||||
@ -30,9 +35,9 @@ const renderNpmTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderPnpmTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
const renderPnpmTab = (scope: string | undefined, registryUrl: string): JSX.Element => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" p={1}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<CopyToClipBoard
|
||||
text={getCLISetConfigRegistry(`${NODE_MANAGER.pnpm} set`, scope, registryUrl)}
|
||||
/>
|
||||
@ -42,9 +47,9 @@ const renderPnpmTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderYarnTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
const renderYarnTab = (scope: string | undefined, registryUrl: string): JSX.Element => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" p={1}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<CopyToClipBoard
|
||||
text={getCLISetConfigRegistry(`${NODE_MANAGER.yarn} config set`, scope, registryUrl)}
|
||||
/>
|
||||
@ -52,6 +57,14 @@ const renderYarnTab = (scope: string, registryUrl: string): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderYarnBerryTab = (scope: string | undefined, registryUrl: string): JSX.Element => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="column">
|
||||
<CopyToClipBoard text={getCLISBerryYamlRegistry(scope, registryUrl)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
@ -63,7 +76,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
}));
|
||||
|
||||
export const AccordionContainer = styled('div')({
|
||||
padding: 30,
|
||||
padding: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
});
|
||||
@ -79,7 +92,7 @@ export const LinkContainer = styled('div')({
|
||||
|
||||
export type Props = {
|
||||
registryUrl: string;
|
||||
scope: string;
|
||||
scope: string | undefined;
|
||||
};
|
||||
|
||||
const RegistryInfoContent: FC<Props> = ({ scope, registryUrl }) => {
|
||||
@ -90,15 +103,13 @@ const RegistryInfoContent: FC<Props> = ({ scope, registryUrl }) => {
|
||||
const hasNpm = configOptions?.pkgManagers?.includes('npm');
|
||||
const hasYarn = configOptions?.pkgManagers?.includes('yarn');
|
||||
const hasPnpm = configOptions?.pkgManagers?.includes('pnpm');
|
||||
const hasPkgManagers = hasNpm | hasPnpm | hasYarn;
|
||||
if (!hasPkgManagers || !scope || !registryUrl) {
|
||||
return <AccordionContainer>{t('header.registry-no-conf')}</AccordionContainer>;
|
||||
}
|
||||
|
||||
return hasPkgManagers ? (
|
||||
<AccordionContainer>
|
||||
{hasNpm && (
|
||||
<Accordion>
|
||||
// TODO: we can improve this logic, expanding only one accordion based on which package manager is enabled
|
||||
// feel free to contribute here
|
||||
return (
|
||||
<>
|
||||
<TextContent>{t('packageManagers.description')}</TextContent>
|
||||
<AccordionContainer>
|
||||
<Accordion disabled={!hasNpm}>
|
||||
<AccordionSummary
|
||||
aria-controls="panel1a-content"
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
@ -112,9 +123,7 @@ const RegistryInfoContent: FC<Props> = ({ scope, registryUrl }) => {
|
||||
</CommandContainer>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasYarn && (
|
||||
<Accordion>
|
||||
<Accordion disabled={!hasYarn}>
|
||||
<AccordionSummary
|
||||
aria-controls="panel2a-content"
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
@ -123,14 +132,26 @@ const RegistryInfoContent: FC<Props> = ({ scope, registryUrl }) => {
|
||||
<Typography className={classes.heading}>{'yarn'}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Description>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{t('packageManagers.yarnclassicDetails')}
|
||||
</ReactMarkdown>
|
||||
</Description>
|
||||
<CommandContainer data-testid={'tab-content'}>
|
||||
{renderYarnTab(scope, registryUrl)}
|
||||
</CommandContainer>
|
||||
<Description>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{t('packageManagers.yarnBerryDetails')}
|
||||
</ReactMarkdown>
|
||||
</Description>
|
||||
<CommandContainer data-testid={'tab-content'}>
|
||||
{renderYarnBerryTab(scope, registryUrl)}
|
||||
</CommandContainer>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
{hasPnpm && (
|
||||
<Accordion>
|
||||
|
||||
<Accordion disabled={!hasPnpm}>
|
||||
<AccordionSummary
|
||||
aria-controls="panel3a-content"
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
@ -144,14 +165,15 @@ const RegistryInfoContent: FC<Props> = ({ scope, registryUrl }) => {
|
||||
</CommandContainer>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
<LinkContainer>
|
||||
<Link href="https://verdaccio.org/docs/en/cli-registry" target="_blank">
|
||||
<Typography>{t('header.registry-info-link')}</Typography>
|
||||
</Link>
|
||||
</LinkContainer>
|
||||
</AccordionContainer>
|
||||
) : null;
|
||||
|
||||
<LinkContainer>
|
||||
<Link href="https://verdaccio.org/docs/en/cli-registry" target="_blank">
|
||||
<Typography>{t('header.registry-info-link')}</Typography>
|
||||
</Link>
|
||||
</LinkContainer>
|
||||
</AccordionContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegistryInfoContent;
|
||||
|
@ -0,0 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
|
||||
export const TextContent = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
||||
paddingBottom: '10px',
|
||||
backgroundColor: theme?.palette.background.default,
|
||||
}));
|
||||
|
||||
export const Description = styled('div')<{ theme?: Theme }>(() => ({
|
||||
fontSize: '1rem',
|
||||
fontStyle: 'italic',
|
||||
}));
|
@ -16,7 +16,7 @@ const RegistryInfoDialog: React.FC<Props> = ({ open = false, children, onClose }
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
>
|
||||
<Title disableTypography={true}>{t('dialog.registry-info.title')}</Title>
|
||||
<Title>{t('dialog.registry-info.title')}</Title>
|
||||
<Content>{children}</Content>
|
||||
<DialogActions>
|
||||
<Button color="inherit" id="registryInfo--dialog-close" onClick={onClose}>
|
||||
|
@ -13,3 +13,8 @@ export const Content = styled(DialogContent)<{ theme?: Theme }>(({ theme }) => (
|
||||
padding: '0 24px',
|
||||
backgroundColor: theme?.palette.background.default,
|
||||
}));
|
||||
|
||||
export const TextContent = styled('div')<{ theme?: Theme }>(({ theme }) => ({
|
||||
padding: '10px 24px',
|
||||
backgroundColor: theme?.palette.background.default,
|
||||
}));
|
||||
|
@ -0,0 +1,8 @@
|
||||
// eslint-disable
|
||||
import React from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
export default function ({ children }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
// eslint-enable
|
1
packages/plugins/ui-theme/src/__mocks__/remark-plugin.ts
Normal file
1
packages/plugins/ui-theme/src/__mocks__/remark-plugin.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function () {}
|
@ -1,300 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import withStyles from '@mui/styles/withStyles';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Theme } from 'verdaccio-ui/design-tokens/theme';
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
|
||||
import IconButton from '../IconButton';
|
||||
import MenuItem from '../MenuItem';
|
||||
import Paper from '../Paper';
|
||||
import TextField from '../TextField';
|
||||
import { createFilterOptions } from './useAutoComplete';
|
||||
|
||||
const defaultFilterOptions = createFilterOptions();
|
||||
|
||||
type TextFieldProps = React.ComponentProps<typeof TextField>;
|
||||
|
||||
interface Props<Option extends {}> extends Pick<TextFieldProps, 'variant'> {
|
||||
options: Option[];
|
||||
getOptionLabel: (option: Option) => string;
|
||||
renderOption?: (option: Option) => React.ReactNode;
|
||||
placeholder?: string;
|
||||
label?: React.ReactNode;
|
||||
defaultValue?: string;
|
||||
inputStartAdornment?: React.ReactNode;
|
||||
hasClearIcon?: boolean;
|
||||
className?: string;
|
||||
onClick?: (option: any) => void;
|
||||
}
|
||||
|
||||
const AutoComplete = <Option extends {}>({
|
||||
placeholder,
|
||||
defaultValue = '',
|
||||
label,
|
||||
variant,
|
||||
inputStartAdornment,
|
||||
options: suggestions,
|
||||
getOptionLabel,
|
||||
renderOption: renderOptionProp,
|
||||
className,
|
||||
onClick,
|
||||
hasClearIcon,
|
||||
}: Props<Option>) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState(defaultValue);
|
||||
const [options, setOptions] = useState(suggestions);
|
||||
const [activeOption, setActiveOption] = useState(0);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
||||
// refs
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const clickOutside = useCallback(() => {
|
||||
setShowOptions(false);
|
||||
if (!searchTerm.trim()) {
|
||||
setSearchTerm(defaultValue);
|
||||
}
|
||||
}, [searchTerm, defaultValue]);
|
||||
|
||||
const filterOptions = useCallback(() => {
|
||||
const filteredOptions = defaultFilterOptions(suggestions, {
|
||||
inputValue: searchTerm,
|
||||
getOptionLabel,
|
||||
});
|
||||
setOptions(filteredOptions);
|
||||
}, [suggestions, searchTerm, getOptionLabel]);
|
||||
|
||||
const scrollIntoOption = useCallback(() => {
|
||||
const option = contentRef?.current?.children[activeOption];
|
||||
if (option) {
|
||||
option.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
}
|
||||
}, [activeOption]);
|
||||
|
||||
useOnClickOutside(wrapperRef, useCallback(clickOutside, [wrapperRef, searchTerm]));
|
||||
|
||||
useEffect(filterOptions, [searchTerm]);
|
||||
|
||||
useEffect(scrollIntoOption, [activeOption]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleToggleShowOptions = useCallback(() => {
|
||||
setShowOptions(!showOptions);
|
||||
}, [showOptions]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setSearchTerm('');
|
||||
setOptions([]);
|
||||
setShowOptions(true);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClickOption = useCallback(
|
||||
(option: Option) => () => {
|
||||
if (onClick) {
|
||||
onClick(option);
|
||||
}
|
||||
setSearchTerm(getOptionLabel(option));
|
||||
setShowOptions(false);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
},
|
||||
[getOptionLabel, onClick]
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setShowOptions(true);
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// User pressed the enter key
|
||||
if (event.keyCode === 13) {
|
||||
setActiveOption(0);
|
||||
setShowOptions(false);
|
||||
handleClickOption(options[activeOption])();
|
||||
return;
|
||||
}
|
||||
|
||||
// User pressed the up arrow
|
||||
if (event.keyCode === 38) {
|
||||
if (activeOption === 0) {
|
||||
return;
|
||||
}
|
||||
setActiveOption(activeOption - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// User pressed the down arrow
|
||||
if (event.keyCode === 40) {
|
||||
if (activeOption + 1 >= options.length) {
|
||||
return;
|
||||
}
|
||||
setActiveOption(activeOption + 1);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[activeOption, handleClickOption, options]
|
||||
);
|
||||
|
||||
const renderOptions = useCallback(() => {
|
||||
if (renderOptionProp) {
|
||||
return options.map((option, index) => (
|
||||
<Option
|
||||
isSelected={index === activeOption}
|
||||
key={index}
|
||||
onClick={handleClickOption(option)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{renderOptionProp(option)}
|
||||
</Option>
|
||||
));
|
||||
}
|
||||
|
||||
return options.map((option, index) => (
|
||||
<MenuItem
|
||||
component="div"
|
||||
key={index}
|
||||
onClick={handleClickOption(option)}
|
||||
selected={index === activeOption}
|
||||
tabIndex={0}
|
||||
>
|
||||
{getOptionLabel(option)}
|
||||
</MenuItem>
|
||||
));
|
||||
}, [options, activeOption, getOptionLabel, renderOptionProp, handleClickOption]);
|
||||
|
||||
return (
|
||||
<Wrapper className={className} ref={wrapperRef}>
|
||||
<StyledTextField
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
startAdornment: inputStartAdornment,
|
||||
endAdornment: (
|
||||
<EndAdornment>
|
||||
{hasClearIcon && searchTerm.length > 0 && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={handleClear}
|
||||
size="small"
|
||||
title={t('autoComplete.clear')}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<ExpandButton
|
||||
color="inherit"
|
||||
onClick={handleToggleShowOptions}
|
||||
showOptions={showOptions}
|
||||
size="small"
|
||||
title={showOptions ? t('autoComplete.collapse') : t('autoComplete.expand')}
|
||||
>
|
||||
<ExpandMoreIcon fontSize="small" />
|
||||
</ExpandButton>
|
||||
</EndAdornment>
|
||||
),
|
||||
}}
|
||||
inputRef={inputRef}
|
||||
label={label}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
variant={variant}
|
||||
/>
|
||||
{showOptions && (
|
||||
<StyledPaper ref={contentRef} square={true}>
|
||||
{renderOptions()}
|
||||
</StyledPaper>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedAutoComplete = memo(AutoComplete) as typeof AutoComplete;
|
||||
|
||||
export { MemoizedAutoComplete as AutoComplete };
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
position: relative;
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
const EndAdornment = styled('div')({
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledTextField = styled(TextField)<{ theme?: Theme }>(({ theme }) => ({
|
||||
height: 40,
|
||||
color: theme?.palette.white,
|
||||
['& .MuiInputBase-root']: {
|
||||
height: 40,
|
||||
color: theme?.palette.white,
|
||||
},
|
||||
['& .MuiInputBase-inputAdornedStart']: {
|
||||
paddingLeft: theme?.spacing(2),
|
||||
},
|
||||
['& .MuiInputBase-input']: {
|
||||
paddingTop: theme?.spacing(1),
|
||||
paddingBottom: theme?.spacing(1),
|
||||
},
|
||||
['& .MuiOutlinedInput-notchedOutline']: {
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
['& :hover .MuiOutlinedInput-notchedOutline']: {
|
||||
borderColor: theme?.palette.white,
|
||||
},
|
||||
['& :active .MuiOutlinedInput-notchedOutline']: {
|
||||
borderColor: theme?.palette.white,
|
||||
},
|
||||
}));
|
||||
|
||||
const ExpandButton = styled(IconButton, {
|
||||
shouldForwardProp: (prop) => prop !== 'showOptions',
|
||||
})<{ showOptions: boolean }>(({ showOptions }) => ({
|
||||
transform: showOptions ? 'rotate(180deg)' : 'none',
|
||||
}));
|
||||
|
||||
const Option = styled('div')<{ isSelected: boolean }>(({ isSelected }) => ({
|
||||
background: isSelected ? 'rgba(0, 0, 0, 0.08)' : 'none',
|
||||
}));
|
||||
|
||||
const StyledPaper = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
marginTop: theme?.spacing(0.5),
|
||||
position: 'absolute',
|
||||
borderRadius: 3,
|
||||
maxHeight: 165,
|
||||
overflowY: 'scroll',
|
||||
zIndex: 10000,
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
}))(Paper);
|
@ -1,61 +0,0 @@
|
||||
// https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
|
||||
// Give up on IE 11 support for this feature
|
||||
function stripDiacritics(value: string) {
|
||||
return typeof value.normalize !== 'undefined'
|
||||
? value.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
: value;
|
||||
}
|
||||
|
||||
// Based on https://github.com/netochaves/material-ui/tree/test/useAutocomplete/packages/material-ui-lab/src/useAutocomplete
|
||||
type CreateFilterOptionsConfig = {
|
||||
ignoreAccents?: boolean;
|
||||
ignoreCase?: boolean;
|
||||
limit?: number;
|
||||
matchFrom?: 'any' | 'start';
|
||||
trim?: boolean;
|
||||
};
|
||||
|
||||
export interface FilterOptionsState<Option> {
|
||||
inputValue: string;
|
||||
getOptionLabel: (option: Option) => string;
|
||||
}
|
||||
|
||||
function createFilterOptions(config?: CreateFilterOptionsConfig) {
|
||||
const {
|
||||
ignoreAccents = true,
|
||||
ignoreCase = true,
|
||||
trim = false,
|
||||
limit,
|
||||
matchFrom = 'any',
|
||||
} = config || {};
|
||||
|
||||
return <Option extends {}>(
|
||||
options: Option[],
|
||||
{ inputValue, getOptionLabel }: FilterOptionsState<Option>
|
||||
): Option[] => {
|
||||
let input = trim ? inputValue.trim() : inputValue;
|
||||
if (ignoreCase) {
|
||||
input = input.toLowerCase();
|
||||
}
|
||||
|
||||
if (ignoreAccents) {
|
||||
input = stripDiacritics(input);
|
||||
}
|
||||
|
||||
const filteredOptions = options.filter((option) => {
|
||||
let candidate = getOptionLabel(option);
|
||||
if (ignoreCase) {
|
||||
candidate = candidate.toLowerCase();
|
||||
}
|
||||
if (ignoreAccents) {
|
||||
candidate = stripDiacritics(candidate);
|
||||
}
|
||||
|
||||
return matchFrom === 'start' ? candidate.indexOf(input) === 0 : candidate.indexOf(input) > -1;
|
||||
});
|
||||
|
||||
return typeof limit === 'number' ? filteredOptions.slice(0, limit) : filteredOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export { createFilterOptions };
|
@ -3,8 +3,8 @@ import React, { forwardRef } from 'react';
|
||||
|
||||
type CardContentRef = HTMLElementTagNameMap[keyof HTMLElementTagNameMap];
|
||||
|
||||
const CardContent = forwardRef<CardContentRef, CardContentProps>(function CardContent(props, ref) {
|
||||
return <MaterialUICardContent {...props} innerRef={ref} />;
|
||||
const CardContent = forwardRef<CardContentRef, CardContentProps>(function CardContent(props) {
|
||||
return <MaterialUICardContent {...props} />;
|
||||
});
|
||||
|
||||
export default CardContent;
|
||||
|
@ -3,8 +3,8 @@ import React, { forwardRef } from 'react';
|
||||
|
||||
type ChipRef = HTMLElementTagNameMap[keyof HTMLElementTagNameMap];
|
||||
|
||||
const Chip = forwardRef<ChipRef, ChipProps>(function Chip(props, ref) {
|
||||
return <MaterialUIChip {...props} innerRef={ref} />;
|
||||
const Chip = forwardRef<ChipRef, ChipProps>(function Chip(props) {
|
||||
return <MaterialUIChip {...props} />;
|
||||
});
|
||||
|
||||
export default Chip;
|
||||
|
@ -37,8 +37,8 @@ const Wrapper = styled('div')({
|
||||
const Content = styled('span')({
|
||||
display: 'inline-block',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
height: '21px',
|
||||
height: 'auto',
|
||||
whiteSpace: 'break-spaces',
|
||||
fontSize: '1rem',
|
||||
});
|
||||
|
@ -11,12 +11,12 @@ type Props = Omit<TooltipProps, 'title'> & {
|
||||
title: TFunctionResult;
|
||||
};
|
||||
|
||||
const Tooltip = forwardRef<TooltipRef, Props>(function ToolTip({ title, children, ...props }, ref) {
|
||||
const Tooltip = forwardRef<TooltipRef, Props>(function ToolTip({ title, children, ...props }) {
|
||||
if (!title) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<MaterialUITooltip {...props} title={title} innerRef={ref}>
|
||||
<MaterialUITooltip {...props} title={title}>
|
||||
{children}
|
||||
</MaterialUITooltip>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { adaptV4Theme, createTheme } from '@mui/material/styles';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { PRIMARY_COLOR } from 'verdaccio-ui/utils/colors';
|
||||
|
||||
const colors = {
|
||||
@ -92,30 +92,28 @@ type CustomizedTheme = typeof customizedTheme;
|
||||
|
||||
export const getTheme = (themeMode: ThemeMode, primaryColor: string) => {
|
||||
const palette = applyPrimaryColor(themeMode, primaryColor);
|
||||
return createTheme(
|
||||
adaptV4Theme({
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
].join(','),
|
||||
return createTheme({
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
].join(','),
|
||||
},
|
||||
palette: {
|
||||
mode: themeMode,
|
||||
...palette,
|
||||
primary: { main: palette.primary },
|
||||
secondary: { main: palette.secondary },
|
||||
error: { main: palette.red },
|
||||
background: {
|
||||
default: palette.background,
|
||||
},
|
||||
palette: {
|
||||
mode: themeMode,
|
||||
...palette,
|
||||
primary: { main: palette.primary },
|
||||
secondary: { main: palette.secondary },
|
||||
error: { main: palette.red },
|
||||
background: {
|
||||
default: palette.background,
|
||||
},
|
||||
},
|
||||
...customizedTheme,
|
||||
})
|
||||
);
|
||||
},
|
||||
...customizedTheme,
|
||||
});
|
||||
};
|
||||
|
||||
export type Theme = ReturnType<typeof getTheme>;
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"dialog": {
|
||||
"registry-info": {
|
||||
"title": "Registry Info"
|
||||
"title": "Configuration Details"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -166,5 +166,16 @@
|
||||
"china": "China",
|
||||
"germany": "Germany",
|
||||
"taiwan": "Taiwan"
|
||||
},
|
||||
"packageManagers": {
|
||||
"title": "Package Managers",
|
||||
"description": "This is the configuration details for the registry. Each package manager could have different configuration, expand each section for more details. If the section is disable review your configuration.",
|
||||
"yarnclassicDetails": "Yarn classic configuration differs from Yarn 2+ configuration. For more details, please visit [Yarn Classic](https://verdaccio.org/docs/cli-registry#yarn-1x).",
|
||||
"yarnBerryDetails": "Yarn Berry does not support the `--registry` flag, instead all configurarion should be defined on the `yarnrc.yalm` file in the root of your project. For more details, please visit [Yarn Berry](https://verdaccio.org/docs/cli-registry#yarn-berry-2x)."
|
||||
},
|
||||
"language": {
|
||||
"title": "Translations",
|
||||
"contribute": "If your language is not listed here or is not fully translated, please contribute to the translation. You can find the source code on [our main repository](https://github.com/verdaccio/verdaccio/blob/master/packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md)",
|
||||
"description": "This is the configuration details for the language. Select a language to select a client side translation."
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,9 @@ exports[`<Help /> component should load the component in default state 1`] = `
|
||||
.emotion-9 {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
height: 21px;
|
||||
height: auto;
|
||||
white-space: break-spaces;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
@ -22,11 +22,11 @@ export const copyToClipBoardUtility =
|
||||
|
||||
export function getCLISetConfigRegistry(
|
||||
command: string,
|
||||
scope: string,
|
||||
scope: string | undefined,
|
||||
registryUrl: string
|
||||
): string {
|
||||
// if there is a scope defined there needs to be a ":" separator between the scope and the registry
|
||||
return `${command} ${scope}${scope !== '' ? ':' : ''}registry ${registryUrl}`;
|
||||
return `${command} ${scope ? `${scope}:` : ''}registry ${registryUrl}`;
|
||||
}
|
||||
|
||||
export function getCLISetRegistry(command: string, registryUrl: string): string {
|
||||
@ -36,3 +36,18 @@ export function getCLISetRegistry(command: string, registryUrl: string): string
|
||||
export function getCLIChangePassword(command: string, registryUrl: string): string {
|
||||
return `${command} profile set password --registry ${registryUrl}`;
|
||||
}
|
||||
|
||||
export function getCLISBerryYamlRegistry(scope: string | undefined, registryUrl: string): string {
|
||||
return !scope
|
||||
? `// .yarnrc.yml
|
||||
npmRegistryServer: "${registryUrl}"
|
||||
unsafeHttpWhitelist:
|
||||
- ${new URL(registryUrl).host}`
|
||||
: `
|
||||
// .yarnrc.yml
|
||||
npmRegistryServer: "${registryUrl}"
|
||||
npmScopes:
|
||||
${scope.replace('@', '')}:
|
||||
npmRegistryServer: ${registryUrl}
|
||||
npmPublishRegistry: ${registryUrl}`;
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ web:
|
||||
title: Verdaccio Local Dev
|
||||
sort_packages: asc
|
||||
primary_color: #CCC
|
||||
login: true
|
||||
login: false
|
||||
pkgManagers:
|
||||
- npm
|
||||
- yarn
|
||||
- pnpm
|
||||
|
||||
plugins: ../
|
||||
|
||||
|
@ -39,7 +39,6 @@ export default {
|
||||
__UI_OPTIONS: JSON.stringify({
|
||||
...configJsonFormat.web,
|
||||
filename: 'index.html',
|
||||
verdaccioURL: '//localhost:4873',
|
||||
base: new URL('/', 'http://localhost:4873'),
|
||||
}),
|
||||
template: `${env.SRC_ROOT}/template/index.html`,
|
||||
|
@ -56,7 +56,6 @@ const prodConf = {
|
||||
primary_color: 'ToReplaceByPrimaryColor',
|
||||
filename: 'index.html',
|
||||
favicon: `${env.SRC_ROOT}/template/favicon.ico`,
|
||||
verdaccioURL: 'ToReplaceByVerdaccio',
|
||||
version_app: 'ToReplaceByVersion',
|
||||
template: `${env.SRC_ROOT}/template/index.html`,
|
||||
debug: false,
|
||||
|
648
pnpm-lock.yaml
generated
648
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,11 @@ web:
|
||||
rateLimit:
|
||||
windowMs: 50000
|
||||
max: 1000
|
||||
pkgManagers:
|
||||
- npm
|
||||
- yarn
|
||||
- pnpm
|
||||
login: true
|
||||
```
|
||||
|
||||
All access restrictions defined to [protect your packages](protect-your-dependencies.md) will also apply to the Web Interface.
|
||||
@ -55,6 +60,8 @@ i18n:
|
||||
| darkMode | boolean | No | false | `>=v4.6.0` | This mode is an special theme for those want to live in the dark side |
|
||||
| favicon | string | No | false | `>=v5.0.1` | Display a custom favicon, can be local resource or valid url |
|
||||
| rateLimit | object | No | use `userRateLimit` configuration | `>=v5.4.0` | Increase or decrease rate limit, by default is 5k request every 2 minutes, only limit web api endpoints, the CSS, JS, etcc are ingnored |
|
||||
| pkgManagers | npm, pnpm or yarn | false | npm | `>=v5.5.0` | Allow customise which package managers on the side bar and registry information dialog are visible |
|
||||
| login | boolean | true | true or false | `>=v5.5.0` | Allow disable login on the UI (also include web endpoints). |
|
||||
|
||||
> The recommended logo size is `40x40` pixels.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user