1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(app): migrate app parent view to react [EE-5361] (#10086)

Co-authored-by: testa113 <testa113>
This commit is contained in:
Ali 2023-08-27 23:01:35 +02:00 committed by GitHub
parent 531f88b947
commit 841ca1ebd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1448 additions and 810 deletions

View file

@ -1,6 +1,6 @@
import { Meta } from '@storybook/react';
import { Badge, Props } from './Badge';
import { Badge, BadgeType, Props } from './Badge';
export default {
component: Badge,
@ -17,11 +17,15 @@ export default {
// : JSX.IntrinsicAttributes & PropsWithChildren<Props>
function Template({ type = 'success' }: Props) {
const message = {
const message: Record<BadgeType, string> = {
success: 'success badge',
danger: 'danger badge',
warn: 'warn badge',
info: 'info badge',
successSecondary: 'successSecondary badge',
dangerSecondary: 'dangerSecondary badge',
warnSecondary: 'warnSecondary badge',
infoSecondary: 'infoSecondary badge',
};
return <Badge type={type}>{message[type]}</Badge>;
}

View file

@ -1,7 +1,60 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
export type BadgeType = 'success' | 'danger' | 'warn' | 'info';
export type BadgeType =
| 'success'
| 'danger'
| 'warn'
| 'info'
| 'successSecondary'
| 'dangerSecondary'
| 'warnSecondary'
| 'infoSecondary';
// the classes are typed in full because tailwind doesn't render the interpolated classes
const typeClasses: Record<BadgeType, string> = {
success: clsx(
`text-success-9 bg-success-2`,
`th-dark:text-success-3 th-dark:bg-success-10`,
`th-highcontrast:text-success-3 th-highcontrast:bg-success-10`
),
warn: clsx(
`text-warning-9 bg-warning-2`,
`th-dark:text-warning-3 th-dark:bg-warning-10`,
`th-highcontrast:text-warning-3 th-highcontrast:bg-warning-10`
),
danger: clsx(
`text-error-9 bg-error-2`,
`th-dark:text-error-3 th-dark:bg-error-10`,
`th-highcontrast:text-error-3 th-highcontrast:bg-error-10`
),
info: clsx(
`text-blue-9 bg-blue-2`,
`th-dark:text-blue-3 th-dark:bg-blue-10`,
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-10`
),
// the secondary classes are a bit darker in light mode and a bit lighter in dark mode
successSecondary: clsx(
`text-success-9 bg-success-3`,
`th-dark:text-success-3 th-dark:bg-success-9`,
`th-highcontrast:text-success-3 th-highcontrast:bg-success-9`
),
warnSecondary: clsx(
`text-warning-9 bg-warning-3`,
`th-dark:text-warning-3 th-dark:bg-warning-9`,
`th-highcontrast:text-warning-3 th-highcontrast:bg-warning-9`
),
dangerSecondary: clsx(
`text-error-9 bg-error-3`,
`th-dark:text-error-3 th-dark:bg-error-9`,
`th-highcontrast:text-error-3 th-highcontrast:bg-error-9`
),
infoSecondary: clsx(
`text-blue-9 bg-blue-3`,
`th-dark:text-blue-3 th-dark:bg-blue-9`,
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-9`
),
};
export interface Props {
type?: BadgeType;
@ -10,50 +63,17 @@ export interface Props {
// this component is used in tables and lists in portainer. It looks like this:
// https://www.figma.com/file/g5TUMngrblkXM7NHSyQsD1/New-UI?node-id=76%3A2
export function Badge({ type, className, children }: PropsWithChildren<Props>) {
export function Badge({
type = 'info',
className,
children,
}: PropsWithChildren<Props>) {
const baseClasses =
'flex w-fit items-center !text-xs font-medium rounded-full px-2 py-0.5';
const typeClasses = getClasses(type);
return (
<span className={clsx(baseClasses, typeClasses, className)}>
<span className={clsx(baseClasses, typeClasses[type], className)}>
{children}
</span>
);
}
// the classes are typed in full to prevent a dev server bug, where tailwind doesn't render the interpolated classes
function getClasses(type: BadgeType | undefined) {
switch (type) {
case 'success':
return clsx(
`text-success-9 bg-success-2`,
`th-dark:text-success-3 th-dark:bg-success-10`,
`th-highcontrast:text-success-3 th-highcontrast:bg-success-10`
);
case 'warn':
return clsx(
`text-warning-9 bg-warning-2`,
`th-dark:text-warning-3 th-dark:bg-warning-10`,
`th-highcontrast:text-warning-3 th-highcontrast:bg-warning-10`
);
case 'danger':
return clsx(
`text-error-9 bg-error-2`,
`th-dark:text-error-3 th-dark:bg-error-10`,
`th-highcontrast:text-error-3 th-highcontrast:bg-error-10`
);
case 'info':
return clsx(
`text-blue-9 bg-blue-2`,
`th-dark:text-blue-3 th-dark:bg-blue-10`,
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-10`
);
default:
return clsx(
`text-blue-9 bg-blue-2`,
`th-dark:text-blue-3 th-dark:bg-blue-10`,
`th-highcontrast:text-blue-3 th-highcontrast:bg-blue-10`
);
}
}

View file

@ -90,6 +90,7 @@ export function CodeEditor({
</div>
<CopyButton
fadeDelay={2500}
copyText={value}
color="link"
className="!pr-0 !text-sm !font-medium hover:no-underline focus:no-underline"

View file

@ -4,17 +4,31 @@ import clsx from 'clsx';
import { Icon } from '@@/Icon';
type Size = 'xs' | 'sm' | 'md';
export type Props = {
className: string;
className?: string;
size?: Size;
};
const sizeStyles: Record<Size, string> = {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-md',
};
export function InlineLoader({
children,
className,
size = 'sm',
}: PropsWithChildren<Props>) {
return (
<div
className={clsx('text-muted flex items-center gap-2 text-sm', className)}
className={clsx(
'text-muted flex items-center gap-2',
className,
sizeStyles[size]
)}
>
<Icon icon={Loader2} className="animate-spin-slow" />
{children}

View file

@ -0,0 +1,89 @@
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { ChevronUp, ChevronRight, Edit } from 'lucide-react';
import { Button } from '@@/buttons';
import { FormError } from '@@/form-components/FormError';
export type Props = {
onChange: (value: string) => void;
defaultIsOpen?: boolean;
value?: string;
labelClass?: string;
inputClass?: string;
isRequired?: boolean;
minLength?: number;
isExpandable?: boolean;
};
export function Note({
onChange,
defaultIsOpen,
value,
labelClass = 'col-sm-12 mb-2',
inputClass = 'col-sm-12',
isRequired,
minLength,
isExpandable,
}: Props) {
const [isNoteOpen, setIsNoteOpen] = useState(defaultIsOpen || isRequired);
useEffect(() => {
setIsNoteOpen(defaultIsOpen || isRequired);
}, [defaultIsOpen, isRequired]);
let error = '';
if (isRequired && minLength && (!value || value.length < minLength)) {
error = `You have entered ${value ? value.length : 0} of the ${minLength} ${
minLength === 1 ? 'character' : 'characters'
} minimum required.`;
}
return (
<div className="form-group">
<div className={clsx('vertical-center', labelClass)}>
{isExpandable && (
<Button
size="small"
type="button"
color="none"
data-cy="k8sAppDetail-expandNoteButton"
onClick={() => setIsNoteOpen(!isNoteOpen)}
className="!m-0 !p-0"
>
{isNoteOpen ? <ChevronUp /> : <ChevronRight />} <Edit />
<span className={isRequired ? 'required' : ''}>Note</span>
</Button>
)}
{!isExpandable && (
<span
className={clsx(
'control-label text-left',
isRequired ? 'required' : ''
)}
>
Note
</span>
)}
</div>
{isNoteOpen && (
<div className={inputClass}>
<textarea
className="form-control resize-y"
name="application_note"
id="application_note"
value={value}
onChange={(e) => onChange(e.target.value)}
rows={5}
placeholder="Enter a note about this application..."
minLength={minLength}
/>
{error && (
<FormError className="error-inline mt-2">{error}</FormError>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1 @@
export { Note, type Props } from './Note';

View file

@ -6,7 +6,7 @@ import { Icon } from '@@/Icon';
import { Link } from '@@/Link';
export interface Tab {
name: string;
name: ReactNode;
icon: ReactNode;
widget: ReactNode;
selectedTabParam: string;
@ -17,6 +17,7 @@ interface Props {
tabs: Tab[];
}
// https://www.figma.com/file/g5TUMngrblkXM7NHSyQsD1/New-UI?type=design&node-id=148-2676&mode=design&t=JKyBWBupeC5WADk6-0
export function WidgetTabs({ currentTabIndex, tabs }: Props) {
// ensure that the selectedTab param is always valid
const invalidQueryParamValue = tabs.every(
@ -37,10 +38,13 @@ export function WidgetTabs({ currentTabIndex, tabs }: Props) {
params={{ tab: tabs[index].selectedTabParam }}
key={index}
className={clsx(
'inline-flex items-center gap-2 border-0 border-b-2 border-solid bg-transparent px-4 py-2',
currentTabIndex === index
? 'border-blue-8 text-blue-8 th-highcontrast:border-blue-6 th-highcontrast:text-blue-6 th-dark:border-blue-6 th-dark:text-blue-6'
: 'border-transparent'
'inline-flex items-center gap-2 border-0 border-b-2 border-solid bg-transparent px-4 py-2 hover:no-underline',
{
'border-blue-8 text-blue-8 th-highcontrast:border-blue-6 th-highcontrast:text-blue-6 th-dark:border-blue-6 th-dark:text-blue-6':
currentTabIndex === index,
'border-transparent text-gray-7 hover:text-gray-8 th-highcontrast:text-gray-6 hover:th-highcontrast:text-gray-5 th-dark:text-gray-6 hover:th-dark:text-gray-5':
currentTabIndex !== index,
}
)}
>
<Icon icon={icon} />