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:
parent
531f88b947
commit
841ca1ebd4
42 changed files with 1448 additions and 810 deletions
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
89
app/react/components/Note/Note.tsx
Normal file
89
app/react/components/Note/Note.tsx
Normal 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>
|
||||
);
|
||||
}
|
1
app/react/components/Note/index.ts
Normal file
1
app/react/components/Note/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Note, type Props } from './Note';
|
|
@ -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} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue