mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518)
This commit is contained in:
parent
769c8372fb
commit
fd916bc8a2
52 changed files with 692 additions and 285 deletions
18
app/react/components/InsightsBox/InsightsBox.stories.tsx
Normal file
18
app/react/components/InsightsBox/InsightsBox.stories.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
|
||||
import { InsightsBox, Props } from './InsightsBox';
|
||||
|
||||
export default {
|
||||
component: InsightsBox,
|
||||
header: 'Components/InsightsBox',
|
||||
} as Meta;
|
||||
|
||||
function Template({ header, content }: Props) {
|
||||
return <InsightsBox header={header} content={content} />;
|
||||
}
|
||||
|
||||
export const Primary: Story<Props> = Template.bind({});
|
||||
Primary.args = {
|
||||
header: 'Insights box header',
|
||||
content: 'This is the content of the insights box',
|
||||
};
|
16
app/react/components/InsightsBox/InsightsBox.test.tsx
Normal file
16
app/react/components/InsightsBox/InsightsBox.test.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { render } from '@testing-library/react';
|
||||
|
||||
import { InsightsBox } from './InsightsBox';
|
||||
|
||||
test('should display a InsightsBox with a header and content', async () => {
|
||||
const header = 'test header';
|
||||
const content = 'test content';
|
||||
const { findByText } = render(
|
||||
<InsightsBox header={header} content={content} />
|
||||
);
|
||||
|
||||
const headerFound = await findByText(header);
|
||||
expect(headerFound).toBeTruthy();
|
||||
const contentFound = await findByText(content);
|
||||
expect(contentFound).toBeTruthy();
|
||||
});
|
63
app/react/components/InsightsBox/InsightsBox.tsx
Normal file
63
app/react/components/InsightsBox/InsightsBox.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import clsx from 'clsx';
|
||||
import { Lightbulb, X } from 'lucide-react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import sanitize from 'sanitize-html';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { insightStore } from './insights-store';
|
||||
|
||||
export type Props = {
|
||||
header: string;
|
||||
content: ReactNode;
|
||||
setHtmlContent?: boolean;
|
||||
insightCloseId?: string; // set if you want to be able to close the box and not show it again
|
||||
};
|
||||
|
||||
export function InsightsBox({
|
||||
header,
|
||||
content,
|
||||
setHtmlContent,
|
||||
insightCloseId,
|
||||
}: Props) {
|
||||
// allow to close the box and not show it again in local storage with zustand
|
||||
const { addInsightIDClosed, isClosed } = useStore(insightStore);
|
||||
const isInsightClosed = isClosed(insightCloseId);
|
||||
|
||||
// allow angular views to set html messages for the insights box
|
||||
const htmlContent = useMemo(() => {
|
||||
if (setHtmlContent && typeof content === 'string') {
|
||||
// eslint-disable-next-line react/no-danger
|
||||
return <div dangerouslySetInnerHTML={{ __html: sanitize(content) }} />;
|
||||
}
|
||||
return null;
|
||||
}, [setHtmlContent, content]);
|
||||
|
||||
if (isInsightClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full gap-1 rounded-lg bg-gray-modern-3 p-4 text-sm th-highcontrast:bg-legacy-grey-3 th-dark:bg-legacy-grey-3">
|
||||
<div className="shrink-0">
|
||||
<Lightbulb className="h-4 text-warning-7 th-highcontrast:text-warning-6 th-dark:text-warning-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className={clsx('mb-2 font-bold', insightCloseId && 'pr-4')}>
|
||||
{header}
|
||||
</p>
|
||||
<div>{htmlContent || content}</div>
|
||||
</div>
|
||||
{insightCloseId && (
|
||||
<Button
|
||||
icon={X}
|
||||
className="absolute top-2 right-2 flex !text-gray-7 hover:!text-gray-8 th-highcontrast:!text-gray-6 th-highcontrast:hover:!text-gray-5 th-dark:!text-gray-6 th-dark:hover:!text-gray-5"
|
||||
color="link"
|
||||
size="medium"
|
||||
onClick={() => addInsightIDClosed(insightCloseId)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
1
app/react/components/InsightsBox/index.ts
Normal file
1
app/react/components/InsightsBox/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { InsightsBox } from './InsightsBox';
|
35
app/react/components/InsightsBox/insights-store.ts
Normal file
35
app/react/components/InsightsBox/insights-store.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { createStore } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface InsightsStore {
|
||||
insightIDsClosed: string[];
|
||||
addInsightIDClosed: (insightIDClosed: string) => void;
|
||||
isClosed: (insightID?: string) => boolean;
|
||||
}
|
||||
|
||||
export const insightStore = createStore<InsightsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
insightIDsClosed: [],
|
||||
addInsightIDClosed: (insightIDClosed: string) => {
|
||||
set((state) => {
|
||||
const currentIDsClosed = state.insightIDsClosed || [];
|
||||
return { insightIDsClosed: [...currentIDsClosed, insightIDClosed] };
|
||||
});
|
||||
},
|
||||
isClosed: (insightID?: string) => {
|
||||
if (!insightID) {
|
||||
return false;
|
||||
}
|
||||
const currentIDsClosed = get().insightIDsClosed || [];
|
||||
return currentIDsClosed.includes(insightID);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: keyBuilder('insightIDsClosed'),
|
||||
getStorage: () => localStorage,
|
||||
}
|
||||
)
|
||||
);
|
|
@ -1,7 +1,3 @@
|
|||
.items {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.items > * + * {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
@ -24,11 +20,6 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.default-item {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { ComponentType } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ArrowDown, ArrowUp, Trash2 } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { AddButton, Button } from '@@/buttons';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
|
@ -79,18 +79,10 @@ export function InputList<T = DefaultType>({
|
|||
return (
|
||||
<div className={clsx('form-group', styles.root)}>
|
||||
<div className={clsx('col-sm-12', styles.header)}>
|
||||
<div className={clsx('control-label text-left', styles.label)}>
|
||||
<span className="control-label space-right pt-2 text-left !font-bold">
|
||||
{label}
|
||||
{tooltip && <Tooltip message={tooltip} />}
|
||||
</div>
|
||||
{!(isAddButtonHidden || readOnly) && (
|
||||
<AddButton
|
||||
label={addLabel}
|
||||
className="space-left"
|
||||
onClick={handleAdd}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{textTip && (
|
||||
|
@ -99,68 +91,86 @@ export function InputList<T = DefaultType>({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx('col-sm-12', styles.items, 'space-y-4')}>
|
||||
{value.map((item, index) => {
|
||||
const key = itemKeyGetter(item, index);
|
||||
const error = typeof errors === 'object' ? errors[index] : undefined;
|
||||
{value.length > 0 && (
|
||||
<div className="col-sm-12 mt-5 flex flex-col gap-y-5">
|
||||
{value.map((item, index) => {
|
||||
const key = itemKeyGetter(item, index);
|
||||
const error =
|
||||
typeof errors === 'object' ? errors[index] : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
styles.itemLine,
|
||||
{ [styles.hasError]: !!error },
|
||||
'vertical-center'
|
||||
)}
|
||||
>
|
||||
{Item ? (
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
renderItem(
|
||||
item,
|
||||
(value: T) => handleChangeItem(key, value),
|
||||
error
|
||||
)
|
||||
)}
|
||||
<div className={clsx(styles.itemActions, 'items-start')}>
|
||||
{!readOnly && movable && (
|
||||
<>
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={disabled || index === 0}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowUp}
|
||||
/>
|
||||
<Button
|
||||
size="medium"
|
||||
type="button"
|
||||
disabled={disabled || index === value.length - 1}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowDown}
|
||||
/>
|
||||
</>
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
styles.itemLine,
|
||||
{ [styles.hasError]: !!error },
|
||||
'vertical-center'
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={() => handleRemoveItem(key, item)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
>
|
||||
{Item ? (
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
renderItem(
|
||||
item,
|
||||
(value: T) => handleChangeItem(key, value),
|
||||
error
|
||||
)
|
||||
)}
|
||||
<div className="items-start">
|
||||
{!readOnly && movable && (
|
||||
<>
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={disabled || index === 0}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowUp}
|
||||
/>
|
||||
<Button
|
||||
size="medium"
|
||||
type="button"
|
||||
disabled={disabled || index === value.length - 1}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={ArrowDown}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={() => handleRemoveItem(key, item)}
|
||||
className="vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="col-sm-12 mt-5">
|
||||
{!(isAddButtonHidden || readOnly) && (
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
size="small"
|
||||
icon={Plus}
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue