mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +02:00
feat(ui): portainer base component css change [EE-3381] (#7115)
This commit is contained in:
parent
825269c119
commit
f78a6568a6
70 changed files with 999 additions and 1596 deletions
|
@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import './BoxSelectorItem.css';
|
||||
|
||||
|
@ -53,9 +54,10 @@ export function BoxSelectorItem<T extends number | string>({
|
|||
|
||||
<div className="boxselector_header">
|
||||
{!!option.icon && (
|
||||
<i
|
||||
className={clsx(option.icon, 'space-right')}
|
||||
aria-hidden="true"
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
feather={option.featherIcon}
|
||||
className="space-right"
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { FeatureId } from '@/portainer/feature-flags/enums';
|
|||
export interface BoxSelectorOption<T> {
|
||||
id: string;
|
||||
icon: string;
|
||||
featherIcon?: boolean;
|
||||
label: string;
|
||||
description: string;
|
||||
value: T;
|
||||
|
|
|
@ -10,12 +10,6 @@ test('should show provided resource value', async () => {
|
|||
expect(value).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
test('should show provided icon', async () => {
|
||||
const { getByLabelText } = renderComponent(0, 'fa fa-th-list');
|
||||
const icon = getByLabelText('icon');
|
||||
expect(icon).toHaveClass('fa-th-list');
|
||||
});
|
||||
|
||||
test('should show provided resource type', async () => {
|
||||
const { getByLabelText } = renderComponent(0, '', 'Test');
|
||||
const title = getByLabelText('resourceType');
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { Icon, IconProps } from '@/react/components/Icon';
|
||||
|
||||
import { Widget, WidgetBody } from '@@/Widget';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
icon: string;
|
||||
interface Props extends IconProps {
|
||||
value?: number;
|
||||
type: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardItem({ value, icon, type }: Props) {
|
||||
export function DashboardItem({
|
||||
value,
|
||||
icon,
|
||||
type,
|
||||
children,
|
||||
featherIcon,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="col-sm-12 col-md-6" aria-label={type}>
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<div className="widget-icon blue pull-left">
|
||||
<i className={icon} aria-hidden="true" aria-label="icon" />
|
||||
<Icon icon={icon} feather={featherIcon} />
|
||||
</div>
|
||||
<div className="pull-right">{children}</div>
|
||||
<div className="title" aria-label="value">
|
||||
{value}
|
||||
</div>
|
||||
|
|
48
app/react/components/Icon.tsx
Normal file
48
app/react/components/Icon.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import clsx from 'clsx';
|
||||
import { ComponentType, ReactNode, useEffect } from 'react';
|
||||
import featherIcons from 'feather-icons';
|
||||
import { isValidElementType } from 'react-is';
|
||||
|
||||
export interface IconProps {
|
||||
icon: ReactNode | ComponentType<unknown>;
|
||||
featherIcon?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
icon: ReactNode | ComponentType<unknown>;
|
||||
feather?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Icon({ icon, feather, className }: Props) {
|
||||
useEffect(() => {
|
||||
if (feather) {
|
||||
featherIcons.replace();
|
||||
}
|
||||
}, [feather]);
|
||||
|
||||
if (typeof icon !== 'string') {
|
||||
const Icon = isValidElementType(icon) ? icon : null;
|
||||
|
||||
return (
|
||||
<span className={className} aria-hidden="true" role="img">
|
||||
{Icon == null ? <>{icon}</> : <Icon />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (feather) {
|
||||
return (
|
||||
<i
|
||||
data-feather={icon}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<i className={clsx('fa', icon, className)} aria-hidden="true" role="img" />
|
||||
);
|
||||
}
|
|
@ -10,7 +10,7 @@ export interface Crumb {
|
|||
linkParams?: Record<string, unknown>;
|
||||
}
|
||||
interface Props {
|
||||
breadcrumbs: Crumb[];
|
||||
breadcrumbs: (Crumb | string)[];
|
||||
}
|
||||
|
||||
export function Breadcrumbs({ breadcrumbs }: Props) {
|
||||
|
@ -26,7 +26,11 @@ export function Breadcrumbs({ breadcrumbs }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
function renderCrumb(crumb: Crumb) {
|
||||
function renderCrumb(crumb: Crumb | string) {
|
||||
if (typeof crumb === 'string') {
|
||||
return crumb;
|
||||
}
|
||||
|
||||
if (crumb.link) {
|
||||
return (
|
||||
<Link to={crumb.link} params={crumb.linkParams}>
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
.user-links {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.user-links > * + * {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link .link-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link .link-icon {
|
||||
margin-right: 2px;
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
|
||||
import { HeaderContainer } from './HeaderContainer';
|
||||
import { HeaderContent } from './HeaderContent';
|
||||
|
@ -11,7 +9,7 @@ test('should not render without a wrapping HeaderContainer', async () => {
|
|||
.mockImplementation(() => jest.fn());
|
||||
|
||||
function renderComponent() {
|
||||
return render(<HeaderContent />);
|
||||
return renderWithQueryClient(<HeaderContent />);
|
||||
}
|
||||
|
||||
expect(renderComponent).toThrowErrorMatchingSnapshot();
|
||||
|
@ -20,22 +18,14 @@ test('should not render without a wrapping HeaderContainer', async () => {
|
|||
});
|
||||
|
||||
test('should display a HeaderContent', async () => {
|
||||
const username = 'username';
|
||||
const user = new UserViewModel({ Username: username });
|
||||
const userProviderState = { user };
|
||||
const content = 'content';
|
||||
|
||||
const { queryByText } = render(
|
||||
<UserContext.Provider value={userProviderState}>
|
||||
<HeaderContainer>
|
||||
<HeaderContent>{content}</HeaderContent>
|
||||
</HeaderContainer>
|
||||
</UserContext.Provider>
|
||||
const { queryByText } = renderWithQueryClient(
|
||||
<HeaderContainer>
|
||||
<HeaderContent>{content}</HeaderContent>
|
||||
</HeaderContainer>
|
||||
);
|
||||
|
||||
const contentElement = queryByText(content);
|
||||
expect(contentElement).toBeVisible();
|
||||
|
||||
expect(queryByText('my account')).toBeVisible();
|
||||
expect(queryByText('log out')).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -1,43 +1,13 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import styles from './HeaderContent.module.css';
|
||||
import { useHeaderContext } from './HeaderContainer';
|
||||
|
||||
export function HeaderContent({ children }: PropsWithChildren<unknown>) {
|
||||
useHeaderContext();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<div className="breadcrumb-links">
|
||||
<div className="pull-left">{children}</div>
|
||||
{user && !window.ddExtension && (
|
||||
<div className={clsx('pull-right', styles.userLinks)}>
|
||||
<Link to="portainer.account" className={styles.link}>
|
||||
<i
|
||||
className={clsx('fa fa-wrench', styles.linkIcon)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={styles.linkText}>my account</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="portainer.logout"
|
||||
params={{ performApiLogout: true }}
|
||||
className={clsx('text-danger', styles.link)}
|
||||
data-cy="template-logoutButton"
|
||||
>
|
||||
<i
|
||||
className={clsx('fa fa-sign-out-alt', styles.linkIcon)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className={styles.linkText}>log out</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
31
app/react/components/PageHeader/HeaderTitle.module.css
Normal file
31
app/react/components/PageHeader/HeaderTitle.module.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.menu-button {
|
||||
border: 0px;
|
||||
font-size: 17px;
|
||||
background: none;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: var(--bg-dropdown-menu-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ui-grey-1) !important;
|
||||
width: 180px;
|
||||
padding: 5px !important;
|
||||
box-shadow: 0 6px 12px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
display: block;
|
||||
padding: 5px 20px;
|
||||
font-weight: 500;
|
||||
line-height: 1.42857143;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: var(--text-dropdown-menu-color);
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.menu-link:hover {
|
||||
background: var(--bg-dropdown-hover);
|
||||
color: var(--text-dropdown-menu-color);
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { Menu, MenuButton, MenuList, MenuLink } from '@reach/menu-button';
|
||||
import clsx from 'clsx';
|
||||
import { User, ChevronDown } from 'react-feather';
|
||||
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { useHeaderContext } from './HeaderContainer';
|
||||
import styles from './HeaderTitle.module.css';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
@ -16,12 +22,25 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
|
|||
<div className="page white-space-normal">
|
||||
{title}
|
||||
<span className="header_title_content">{children}</span>
|
||||
{user && !window.ddExtension && (
|
||||
<span className="pull-right user-box">
|
||||
<i className="fa fa-user-circle" aria-hidden="true" />
|
||||
{user.Username}
|
||||
</span>
|
||||
)}
|
||||
<Menu>
|
||||
<MenuButton className={clsx('pull-right', styles.menuButton)}>
|
||||
<User className="feather" />
|
||||
{user && <span>{user.Username}</span>}
|
||||
<ChevronDown className="feather" />
|
||||
</MenuButton>
|
||||
<MenuList className={styles.menuList}>
|
||||
<MenuLink
|
||||
className={styles.menuLink}
|
||||
as={Link}
|
||||
to="portainer.account"
|
||||
>
|
||||
My account
|
||||
</MenuLink>
|
||||
<MenuLink className={styles.menuLink} as={Link} to="portainer.logout">
|
||||
Log out
|
||||
</MenuLink>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useRouter } from '@uirouter/react';
|
||||
import { RefreshCw } from 'react-feather';
|
||||
|
||||
import { Button } from '../buttons';
|
||||
|
||||
|
@ -11,12 +12,25 @@ import styles from './PageHeader.module.css';
|
|||
|
||||
interface Props {
|
||||
reload?: boolean;
|
||||
loading?: boolean;
|
||||
onReload?(): Promise<void> | void;
|
||||
breadcrumbs?: Crumb[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, breadcrumbs = [], reload }: Props) {
|
||||
export function PageHeader({
|
||||
title,
|
||||
breadcrumbs = [],
|
||||
reload,
|
||||
loading,
|
||||
onReload,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
function onClickedRefresh() {
|
||||
return onReload ? onReload() : router.stateService.reload();
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<HeaderTitle title={title}>
|
||||
|
@ -24,10 +38,11 @@ export function PageHeader({ title, breadcrumbs = [], reload }: Props) {
|
|||
<Button
|
||||
color="link"
|
||||
size="medium"
|
||||
onClick={() => router.stateService.reload()}
|
||||
onClick={onClickedRefresh}
|
||||
className={styles.reloadButton}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className="fa fa-sync" aria-hidden="true" />
|
||||
<RefreshCw className="feather" />
|
||||
</Button>
|
||||
)}
|
||||
</HeaderTitle>
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
.pagination > .active > span:focus,
|
||||
.pagination > .active > button:focus {
|
||||
z-index: 3;
|
||||
color: #fff;
|
||||
color: #1d2939;
|
||||
cursor: default;
|
||||
background-color: var(--text-pagination-span-color);
|
||||
border-color: var(--text-pagination-span-color);
|
||||
|
|
8
app/react/components/ReactQueryDevtoolsWrapper.tsx
Normal file
8
app/react/components/ReactQueryDevtoolsWrapper.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
|
||||
export function ReactQueryDevtoolsWrapper() {
|
||||
const showReactQueryDevtools =
|
||||
process.env.SHOW_REACT_QUERY_DEV_TOOLS === 'true';
|
||||
|
||||
return <>{showReactQueryDevtools && <ReactQueryDevtools />}</>;
|
||||
}
|
|
@ -4,12 +4,10 @@
|
|||
}
|
||||
|
||||
.tooltip {
|
||||
font-family: Montserrat !important;
|
||||
background-color: var(--bg-tooltip-color) !important;
|
||||
padding: 0.833em 1em !important;
|
||||
color: var(--text-tooltip-color) !important;
|
||||
border: 1px solid var(--border-tooltip-color) !important;
|
||||
border-radius: 0.14285714rem !important;
|
||||
border-radius: 10px !important;
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
|
@ -19,5 +17,5 @@
|
|||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { HelpCircle } from 'react-feather';
|
||||
|
||||
import styles from './Tooltip.module.css';
|
||||
|
||||
|
@ -12,12 +12,8 @@ export interface Props {
|
|||
|
||||
export function Tooltip({ message, position = 'bottom' }: Props) {
|
||||
return (
|
||||
<span className="interactive">
|
||||
<i
|
||||
className={clsx('fa fa-question-circle blue-icon', styles.icon)}
|
||||
aria-hidden="true"
|
||||
data-tip={message}
|
||||
/>
|
||||
<span data-tip={message} className={styles.icon}>
|
||||
<HelpCircle className="feather" aria-hidden="true" />
|
||||
<ReactTooltip
|
||||
multiline
|
||||
type="info"
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import { useWidgetContext } from './Widget';
|
||||
|
||||
interface Props {
|
||||
title: ReactNode;
|
||||
icon: ReactNode;
|
||||
featherIcon?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -14,6 +17,7 @@ export function WidgetTitle({
|
|||
icon,
|
||||
className,
|
||||
children,
|
||||
featherIcon,
|
||||
}: PropsWithChildren<Props>) {
|
||||
useWidgetContext();
|
||||
|
||||
|
@ -21,7 +25,7 @@ export function WidgetTitle({
|
|||
<div className="widget-header">
|
||||
<div className="row">
|
||||
<span className={clsx('pull-left', className)}>
|
||||
{typeof icon === 'string' ? <i className={clsx('fa', icon)} /> : icon}
|
||||
<Icon icon={icon} feather={featherIcon} className="space-right" />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
<span className={clsx('pull-right', className)}>{children}</span>
|
||||
|
|
|
@ -2,7 +2,15 @@ import { MouseEventHandler, PropsWithChildren } from 'react';
|
|||
import clsx from 'clsx';
|
||||
|
||||
type Type = 'submit' | 'button' | 'reset';
|
||||
type Color = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'link';
|
||||
type Color =
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'link'
|
||||
| 'light'
|
||||
| 'dangerlight';
|
||||
type Size = 'xsmall' | 'small' | 'medium' | 'large';
|
||||
|
||||
export interface Props {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
||||
import { ColumnInstance } from 'react-table';
|
||||
import { Columns } from 'react-feather';
|
||||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
|
@ -28,7 +29,13 @@ export function ColumnVisibilityMenu<D extends object>({
|
|||
'setting-active': isExpanded,
|
||||
})}
|
||||
>
|
||||
<i className="fa fa-columns" aria-hidden="true" /> Columns
|
||||
<Columns
|
||||
size="13"
|
||||
className="space-right"
|
||||
strokeWidth="3px"
|
||||
aria-hidden="true"
|
||||
aria-label="Columns"
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<div className="tableMenu">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { MoreVertical } from 'react-feather';
|
||||
|
||||
import { useTableContext } from './TableContainer';
|
||||
|
||||
|
@ -23,7 +24,13 @@ export function TableSettingsMenu({
|
|||
'setting-active': isExpanded,
|
||||
})}
|
||||
>
|
||||
<i className="fa fa-cog" aria-hidden="true" /> Settings
|
||||
<MoreVertical
|
||||
size="13"
|
||||
className="space-right"
|
||||
strokeWidth="3px"
|
||||
aria-hidden="true"
|
||||
aria-label="Settings"
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<div className="tableMenu">
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import { useTableContext } from './TableContainer';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
label: string;
|
||||
featherIcon?: boolean;
|
||||
}
|
||||
|
||||
export function TableTitle({
|
||||
icon,
|
||||
featherIcon,
|
||||
label,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) {
|
||||
|
@ -18,7 +21,8 @@ export function TableTitle({
|
|||
return (
|
||||
<div className="toolBar">
|
||||
<div className="toolBarTitle">
|
||||
<i className={clsx('space-right', 'fa', icon)} aria-hidden="true" />
|
||||
<Icon icon={icon} feather={featherIcon} className="space-right" />
|
||||
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
|
|
|
@ -28,5 +28,5 @@
|
|||
|
||||
.slider :global .rc-slider-mark-text,
|
||||
.slider :global .rc-slider-tooltip-inner {
|
||||
font-family: Montserrat, serif;
|
||||
font-family: Inter, serif;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export function Switch({
|
|||
disabled={disabled || limitedToBE}
|
||||
onChange={({ target: { checked } }) => onChange(checked)}
|
||||
/>
|
||||
<i data-cy={dataCy} />
|
||||
<span className="slider round" data-cy={dataCy} />
|
||||
</label>
|
||||
{limitedToBE && <BEFeatureIndicator featureId={featureId} />}
|
||||
</>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue