mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feat(podman): support add podman envs in the wizard [r8s-20] (#12056)
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
This commit is contained in:
parent
db616bc8a5
commit
32e94d4e4e
108 changed files with 1921 additions and 272 deletions
|
@ -46,7 +46,7 @@ export function Tooltip({
|
|||
position={position}
|
||||
className={className}
|
||||
>
|
||||
<HelpCircle className="lucide" aria-hidden="true" />
|
||||
<HelpCircle className="lucide" />
|
||||
</TooltipWithChildren>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -25,7 +25,7 @@ export function CopyButton({
|
|||
fadeDelay = 1000,
|
||||
displayText = 'copied',
|
||||
className,
|
||||
color,
|
||||
color = 'default',
|
||||
indicatorPosition = 'right',
|
||||
children,
|
||||
'data-cy': dataCy,
|
||||
|
@ -52,7 +52,7 @@ export function CopyButton({
|
|||
<div className={styles.container}>
|
||||
{indicatorPosition === 'left' && copiedIndicator()}
|
||||
<Button
|
||||
className={className}
|
||||
className={clsx(className, '!ml-0')}
|
||||
color={color}
|
||||
size="small"
|
||||
onClick={handleCopy}
|
||||
|
|
302
app/react/components/datatables/Datatable.test.tsx
Normal file
302
app/react/components/datatables/Datatable.test.tsx
Normal file
|
@ -0,0 +1,302 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createColumnHelper,
|
||||
createTable,
|
||||
getCoreRowModel,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { Datatable, defaultGlobalFilterFn, Props } from './Datatable';
|
||||
import {
|
||||
BasicTableSettings,
|
||||
createPersistedStore,
|
||||
refreshableSettings,
|
||||
RefreshableTableSettings,
|
||||
} from './types';
|
||||
import { useTableState } from './useTableState';
|
||||
|
||||
// Mock data and dependencies
|
||||
type MockData = { id: string; name: string; age: number };
|
||||
const mockData = [
|
||||
{ id: '1', name: 'John Doe', age: 30 },
|
||||
{ id: '2', name: 'Jane Smith', age: 25 },
|
||||
{ id: '3', name: 'Bob Johnson', age: 35 },
|
||||
];
|
||||
|
||||
const mockColumns = [
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'age', header: 'Age' },
|
||||
];
|
||||
|
||||
// mock table settings / state
|
||||
export interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
function createStore(storageKey: string) {
|
||||
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
|
||||
...refreshableSettings(set),
|
||||
}));
|
||||
}
|
||||
const storageKey = 'test-table';
|
||||
const settingsStore = createStore(storageKey);
|
||||
const mockSettingsManager = {
|
||||
pageSize: 10,
|
||||
search: '',
|
||||
sortBy: undefined,
|
||||
setSearch: vitest.fn(),
|
||||
setSortBy: vitest.fn(),
|
||||
setPageSize: vitest.fn(),
|
||||
};
|
||||
|
||||
function DatatableWithStore(props: Omit<Props<MockData>, 'settingsManager'>) {
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
return (
|
||||
<Datatable {...props} settingsManager={tableState} data-cy="test-table" />
|
||||
);
|
||||
}
|
||||
|
||||
describe('Datatable', () => {
|
||||
it('renders the table with correct data', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the table with a title', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
title="Test Table"
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles row selection when not disabled', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(checkboxes[1]); // Select the first row
|
||||
|
||||
// Check if the row is selected (you might need to adapt this based on your implementation)
|
||||
expect(checkboxes[1]).toBeChecked();
|
||||
});
|
||||
|
||||
it('disables row selection when disableSelect is true', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
disableSelect
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
const checkboxes = screen.queryAllByRole('checkbox');
|
||||
expect(checkboxes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('handles sorting', () => {
|
||||
render(
|
||||
<Datatable
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
settingsManager={mockSettingsManager}
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
const nameHeader = screen.getByText('Name');
|
||||
fireEvent.click(nameHeader);
|
||||
|
||||
// Check if setSortBy was called with the correct arguments
|
||||
expect(mockSettingsManager.setSortBy).toHaveBeenCalledWith('name', true);
|
||||
});
|
||||
|
||||
it('renders loading state', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={mockData}
|
||||
columns={mockColumns}
|
||||
isLoading
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state', () => {
|
||||
render(
|
||||
<DatatableWithStore
|
||||
dataset={[]}
|
||||
columns={mockColumns}
|
||||
emptyContentLabel="No data available"
|
||||
data-cy="test-table"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No data available')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Test the defaultGlobalFilterFn used in searches
|
||||
type Person = {
|
||||
id: string;
|
||||
name: string;
|
||||
age: number;
|
||||
isEmployed: boolean;
|
||||
tags?: string[];
|
||||
city?: string;
|
||||
family?: { sister: string; uncles?: string[] };
|
||||
};
|
||||
const data: Person[] = [
|
||||
{
|
||||
// searching primitives should be supported
|
||||
id: '1',
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
isEmployed: true,
|
||||
// supporting arrays of primitives should be supported
|
||||
tags: ['music', 'likes-pixar'],
|
||||
// supporting objects of primitives should be supported (values only).
|
||||
// but shouldn't be support nested objects / arrays
|
||||
family: { sister: 'sophie', uncles: ['john', 'david'] },
|
||||
},
|
||||
];
|
||||
const columnHelper = createColumnHelper<Person>();
|
||||
const columns = [
|
||||
columnHelper.accessor('name', {
|
||||
id: 'name',
|
||||
}),
|
||||
columnHelper.accessor('isEmployed', {
|
||||
id: 'isEmployed',
|
||||
}),
|
||||
columnHelper.accessor('age', {
|
||||
id: 'age',
|
||||
}),
|
||||
columnHelper.accessor('tags', {
|
||||
id: 'tags',
|
||||
}),
|
||||
columnHelper.accessor('family', {
|
||||
id: 'family',
|
||||
}),
|
||||
];
|
||||
const mockTable = createTable({
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {},
|
||||
onStateChange() {},
|
||||
renderFallbackValue: undefined,
|
||||
getRowId: (row) => row.id,
|
||||
});
|
||||
const mockRow = mockTable.getRow('1');
|
||||
|
||||
describe('defaultGlobalFilterFn', () => {
|
||||
it('should return true when filterValue is null', () => {
|
||||
const result = defaultGlobalFilterFn(mockRow, 'Name', null);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when filterValue.search is empty', () => {
|
||||
const result = defaultGlobalFilterFn(mockRow, 'Name', {
|
||||
search: '',
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter string values correctly', () => {
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'name', {
|
||||
search: 'hello',
|
||||
})
|
||||
).toBe(false);
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'name', {
|
||||
search: 'ALICE',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'name', {
|
||||
search: 'Alice',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter number values correctly', () => {
|
||||
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '123' })).toBe(
|
||||
false
|
||||
);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '30' })).toBe(true);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '67' })).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter boolean values correctly', () => {
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'isEmployed', { search: 'true' })
|
||||
).toBe(true);
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'isEmployed', { search: 'false' })
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter object values correctly', () => {
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'sophie' })).toBe(
|
||||
true
|
||||
);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: '30' })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter array values correctly', () => {
|
||||
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'music' })).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
defaultGlobalFilterFn(mockRow, 'tags', { search: 'Likes-Pixar' })
|
||||
).toBe(true);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'grape' })).toBe(
|
||||
false
|
||||
);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'likes' })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle complex nested structures', () => {
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'sophie' })).toBe(
|
||||
true
|
||||
);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'mason' })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should not filter non-primitive values within objects and arrays', () => {
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'john' })).toBe(
|
||||
false
|
||||
);
|
||||
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'david' })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
|
@ -272,6 +272,21 @@ export function defaultGlobalFilterFn<D, TFilter extends { search: string }>(
|
|||
|
||||
const filterValueLower = filterValue.search.toLowerCase();
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return Object.values(value).some((item) =>
|
||||
filterPrimitive(item, filterValueLower)
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => filterPrimitive(item, filterValueLower));
|
||||
}
|
||||
|
||||
return filterPrimitive(value, filterValueLower);
|
||||
}
|
||||
|
||||
// only filter primitive values within objects and arrays, to avoid searching nested objects
|
||||
function filterPrimitive(value: unknown, filterValueLower: string) {
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
|
@ -279,13 +294,6 @@ export function defaultGlobalFilterFn<D, TFilter extends { search: string }>(
|
|||
) {
|
||||
return value.toString().toLowerCase().includes(filterValueLower);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) =>
|
||||
item.toString().toLowerCase().includes(filterValueLower)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue